<?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: Narendra | SRE</title>
    <description>The latest articles on DEV Community by Narendra | SRE (@narendrasre).</description>
    <link>https://dev.to/narendrasre</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3725068%2F65247ed8-bd04-4b1a-9123-b2bfa3a25179.jpg</url>
      <title>DEV Community: Narendra | SRE</title>
      <link>https://dev.to/narendrasre</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/narendrasre"/>
    <language>en</language>
    <item>
      <title>Automating AWS Infrastructure Deployment with Terraform</title>
      <dc:creator>Narendra | SRE</dc:creator>
      <pubDate>Tue, 10 Nov 2026 16:21:51 +0000</pubDate>
      <link>https://dev.to/narendrasre/automating-aws-infrastructure-deployment-with-terraform-24kh</link>
      <guid>https://dev.to/narendrasre/automating-aws-infrastructure-deployment-with-terraform-24kh</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Manual cloud provisioning can be difficult to reproduce, review, and audit consistently. Infrastructure as Code (IaC) addresses this by describing the desired infrastructure in version-controlled configuration.&lt;/p&gt;

&lt;p&gt;This project uses Terraform to deploy a reusable, security-conscious Amazon S3 bucket configuration on AWS. It demonstrates provider configuration, variables and outputs, reusable modules, Terraform state, controlled plan/apply/destroy workflows, AWS CLI verification, and automated Terraform checks with GitHub Actions.&lt;/p&gt;

&lt;p&gt;The implementation focuses on practical infrastructure automation while keeping security controls and operational safety visible throughout the workflow.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Cost notice:&lt;/strong&gt; This project provisions real AWS resources and is not guaranteed to be free. S3 storage, requests, and other account activity may incur charges. Review current AWS pricing and your account billing before deployment, and remove demonstration resources when they are no longer needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Implementation status:&lt;/strong&gt; Terraform configuration deployed successfully; the six planned resources were created. Deployed S3 security settings verified using AWS CLI. Evidence command outputs and deployment screenshots captured. Infrastructure cleanup is Completed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  1. What we are building
&lt;/h2&gt;

&lt;p&gt;Terraform creates an S3 bucket with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A globally unique name generated using Terraform's &lt;code&gt;random&lt;/code&gt; provider.&lt;/li&gt;
&lt;li&gt;Versioning enabled.&lt;/li&gt;
&lt;li&gt;Default server-side encryption using SSE-S3.&lt;/li&gt;
&lt;li&gt;All four S3 Block Public Access settings enabled.&lt;/li&gt;
&lt;li&gt;Bucket-owner-enforced object ownership.&lt;/li&gt;
&lt;li&gt;Consistent project and environment tags.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The S3 configuration is encapsulated in a reusable Terraform module.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Architecture and deployment flow
&lt;/h2&gt;

&lt;h4&gt;
  
  
  Deployment Flow
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Developer
   |
   v
Terraform root configuration
   |-- variables.tf
   |-- main.tf
   |-- outputs.tf
   |
   v
Reusable module: secure_s3_bucket
   |-- S3 bucket
   |-- Versioning
   |-- Default encryption
   |-- Block Public Access
   |-- Object ownership controls
   |
   v
AWS provider -&amp;gt; AWS account

Terraform state records managed resources.
GitHub Actions validates configuration; it does not deploy.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&amp;nbsp;&lt;/p&gt;

&lt;h4&gt;
  
  
  Architecture
&lt;/h4&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F96kesw190jf6i9n1to78.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F96kesw190jf6i9n1to78.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Repository structure
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;article-06-terraform-infrastructure-deployment/
├── ARTICLE.md
├── README.md
├── .gitignore
├── .github/
│   └── workflows/
│       └── terraform-checks.yml
├── examples/
│   └── terraform.tfvars.example
├── images/
│   └── architecture.svg
├── evidence/
│   ├── README.md
│   ├── results/
│   │   ├── .gitkeep
│   │   └── [captured command outputs]
│   └── screenshots/
│       ├── [deployment and verification screenshots]
│       └── [GitHub Actions screenshot, if captured]
├── modules/
│   └── secure_s3_bucket/
│       ├── main.tf
│       ├── variables.tf
│       └── outputs.tf
├── scripts/
│   ├── terraform-check.sh
│   ├── plan.sh
│   ├── apply.sh
│   ├── verify.sh
│   ├── capture-evidence.sh
│   └── destroy.sh
└── terraform/
    ├── versions.tf
    ├── providers.tf
    ├── variables.tf
    ├── main.tf
    ├── outputs.tf
    ├── terraform.tfvars.example
    └── .terraform.lock.hcl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;.terraform.lock.hcl&lt;/code&gt; file is generated during initialization and should be committed to version control. Terraform working directories, state files, backup state files, local variable files, and generated plan files should remain excluded through &lt;code&gt;.gitignore&lt;/code&gt;. Evidence files should contain genuine results and must be reviewed for sensitive information before committing.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Reusable Terraform module
&lt;/h2&gt;

&lt;p&gt;The root configuration calls &lt;code&gt;modules/secure_s3_bucket&lt;/code&gt;. The module separates the S3 resource implementation from environment-specific inputs. It accepts a bucket name and tags, then configures versioning, encryption, public-access blocking, and object ownership.&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr49vdi4qmquw6qbo39u5.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr49vdi4qmquw6qbo39u5.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Variables and outputs
&lt;/h2&gt;

&lt;p&gt;Variables allow the same configuration to be reused without editing resource definitions. Inputs include the bucket name prefix, AWS region, environment, and project name.&lt;/p&gt;

&lt;p&gt;A random suffix is added to the bucket name to reduce the chance of a global S3 naming collision. Terraform records the generated value in state. Outputs expose the bucket name, ARN, and region for operators and verification scripts. Do not use outputs to expose secrets.&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Terraform state management
&lt;/h2&gt;

&lt;p&gt;Terraform state maps configuration resources to real infrastructure objects. Terraform uses it to calculate changes during subsequent plans.&lt;/p&gt;

&lt;p&gt;This standalone demonstration uses the default local state file. State can contain sensitive infrastructure metadata:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do not commit &lt;code&gt;terraform.tfstate&lt;/code&gt; or backup state files.&lt;/li&gt;
&lt;li&gt;Do not manually edit state files.&lt;/li&gt;
&lt;li&gt;Protect and back up state.&lt;/li&gt;
&lt;li&gt;For team or production use, prefer a protected remote backend with access controls, encryption, versioning, and locking.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&amp;nbsp;&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj69e2858evladwj77ed0.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj69e2858evladwj77ed0.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This repository documents local state; it does not automatically create a remote backend. A remote backend requires a separately bootstrapped storage location and deliberate access-control design.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Deployment workflow
&lt;/h2&gt;

&lt;p&gt;From the project root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cp terraform/terraform.tfvars.example terraform/terraform.tfvars
terraform fmt -recursive
terraform -chdir=terraform init
./scripts/terraform-check.sh
./scripts/plan.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Review the plan before applying. It should contain only the intended S3 bucket and its security configuration. Do not approve changes you do not recognize.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./scripts/apply.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Terraform asks for confirmation. Read the proposed changes before approving.&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsjmo0qzjsc0htumcik4h.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsjmo0qzjsc0htumcik4h.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhbaunnej035zbjoavkkd.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhbaunnej035zbjoavkkd.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Verify the deployed configuration
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./scripts/verify.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script uses Terraform outputs and AWS CLI read operations to check the bucket's public-access block, versioning, encryption, and ownership controls.&lt;/p&gt;

&lt;p&gt;After deployment, a second Terraform plan should report no changes when the live infrastructure matches the configuration.&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl92lzc5vmgwbrgnq5jdy.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl92lzc5vmgwbrgnq5jdy.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Report only settings that the verification commands actually confirm. A successful apply is not a substitute for checking the resulting configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Automated checks with GitHub Actions
&lt;/h2&gt;

&lt;p&gt;The included GitHub Actions workflow runs Terraform formatting, initialization, and validation when relevant files change. It does not apply infrastructure and does not require AWS credentials.&lt;/p&gt;

&lt;p&gt;A production deployment workflow should use short-lived credentials through GitHub OIDC, a narrowly scoped IAM role, protected environments, and explicit approval gates rather than long-lived access keys.&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcx16hqeoe99htsyc634k.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcx16hqeoe99htsyc634k.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Cost, safety, and cleanup
&lt;/h2&gt;

&lt;p&gt;This project creates an S3 bucket and related S3 configuration resources. Charges may apply for storage, requests, and other AWS usage. Review current AWS pricing and your account billing. Consider setting a budget or billing alert.&lt;/p&gt;

&lt;p&gt;After collecting and reviewing evidence, destroy the demonstration resources if they are no longer needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./scripts/destroy.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The bucket uses &lt;code&gt;force_destroy = false&lt;/code&gt;. If it contains objects or versions, Terraform will not silently empty it. Inspect the bucket and remove only demonstration data you intentionally created, then run destroy again. Do not enable force deletion simply to make cleanup easier.&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbt4pvn5c5ol6csh0o05s.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbt4pvn5c5ol6csh0o05s.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  11. Limitations and next steps
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Local state is used for a single-user demonstration; team use should adopt a protected remote backend.&lt;/li&gt;
&lt;li&gt;GitHub Actions validates configuration but deliberately does not deploy.&lt;/li&gt;
&lt;li&gt;This is a small S3 example, not a complete production landing zone.&lt;/li&gt;
&lt;li&gt;It does not include organization-wide guardrails, centralized logging, or cross-account deployment.&lt;/li&gt;
&lt;li&gt;Future improvements could add policy-as-code checks, environment promotion, remote-state bootstrapping, and an OIDC-based deployment workflow.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Terraform makes infrastructure changes reviewable and repeatable by describing desired state as code. Reusable modules, explicit variables and outputs, a deliberate state strategy, and a plan-before-apply workflow form a practical foundation for AWS infrastructure management.&lt;/p&gt;

&lt;p&gt;Automation is more than running &lt;code&gt;terraform apply&lt;/code&gt;: it includes reviewing changes, protecting state, verifying deployed settings, controlling permissions, understanding cost, and cleaning up responsibly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repository:&lt;/strong&gt; [&lt;a href="https://github.com/narendra-sre/aws-community-builders/tree/main/article-06-terraform-infrastructure-deployment" rel="noopener noreferrer"&gt;AWS Community Builder Repository&lt;/a&gt;]&lt;/p&gt;

</description>
      <category>terraform</category>
      <category>aws</category>
      <category>s3</category>
      <category>automation</category>
    </item>
    <item>
      <title>From Wildcards to Least Privilege: Building a No-Infrastructure AWS IAM Policy Security Review Workflow</title>
      <dc:creator>Narendra | SRE</dc:creator>
      <pubDate>Sun, 08 Nov 2026 12:14:04 +0000</pubDate>
      <link>https://dev.to/narendrasre/from-wildcards-to-least-privilege-building-a-no-infrastructure-aws-iam-policy-security-review-27g1</link>
      <guid>https://dev.to/narendrasre/from-wildcards-to-least-privilege-building-a-no-infrastructure-aws-iam-policy-security-review-27g1</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;IAM policies can grant broader access than an application needs. A statement such as &lt;code&gt;"Action": "*"&lt;/code&gt; combined with &lt;code&gt;"Resource": "*"&lt;/code&gt; is a clear warning sign, while broad service permissions and sensitive IAM actions can also introduce unnecessary privilege.&lt;/p&gt;

&lt;p&gt;This project compares an intentionally over-permissive IAM identity policy with a scoped S3 example. It uses three complementary review stages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A local Python JSON checker that detects a deliberately limited set of risky patterns.&lt;/li&gt;
&lt;li&gt;AWS IAM Access Analyzer policy validation.&lt;/li&gt;
&lt;li&gt;AWS IAM Policy Simulator evaluations for selected action/resource pairs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The workflow is review-only. It does not create IAM identities, attach policies, create an S3 bucket, or provision compute or storage infrastructure. The AWS API calls validate and simulate policy documents; they do not execute S3 operations.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Implementation Status:&lt;/strong&gt; Implementation completed successfully. The Python IAM policy auditor, AWS IAM Access Analyzer validation, and IAM Policy Simulator tests have been executed, and the evidence outputs have been collected for review. The project is now moving into documentation finalization, screenshot collection, and GitHub publication.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost wording:&lt;/strong&gt; This project provisions no AWS infrastructure. Do not interpret that as a guarantee that every account activity is free. Check current AWS pricing and your account's billing information independently.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  1. The problem
&lt;/h2&gt;

&lt;p&gt;A policy can be valid JSON and still grant excessive permissions. For example:&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;"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;"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;"*"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This statement allows all actions on all resources within the scope of the identity policy, subject to the broader AWS authorization evaluation. Another pattern worth reviewing is a sensitive IAM action such as &lt;code&gt;iam:PassRole&lt;/code&gt; allowed against every resource.&lt;/p&gt;

&lt;p&gt;The challenge is to review policies before deployment without attaching an unsafe policy to a real identity or creating test infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Architecture
&lt;/h2&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdwlawyxlql7x4s96v2tm.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdwlawyxlql7x4s96v2tm.png" alt=" " width="800" height="1071"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The workflow has three stages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Developer / DevOps engineer
           |
           v
    IAM policy JSON files
           |
     +-----+----------------+
     |                      |
     v                      v
Over-permissive         Scoped S3
example policy          example policy
     |                      |
     +----------+-----------+
                |
                v
       Stage 1: Local check
        Python JSON auditor
                |
                v
     Stage 2: Policy validation
       IAM Access Analyzer
          ValidatePolicy
                |
                v
      Stage 3: Authorization test
       IAM Policy Simulator
        SimulateCustomPolicy
                |
                v
       Review actual results
                |
                v
        Evidence and reporting

No IAM resources, buckets, or compute are created.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The stages answer different questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Local checker:&lt;/strong&gt; Does the document contain any of the specific risky patterns selected for this project?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Access Analyzer:&lt;/strong&gt; What findings does AWS return when it validates the identity policy?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Policy Simulator:&lt;/strong&gt; What decision does the simulator return for the selected action/resource pairs and supplied policy?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These tools complement one another; none independently proves that a policy is safe for every workload.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Project files
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;policies/overprivileged-policy.json&lt;/code&gt; — deliberately unsafe teaching example. Do not attach it to a real identity.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;policies/least-privilege-policy.json&lt;/code&gt; — scoped S3 permissions for a fictional bucket and telemetry prefix.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;scripts/iam_policy_auditor.py&lt;/code&gt; — limited local JSON policy checker.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;scripts/audit-policies.sh&lt;/code&gt; — runs the local checker.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;scripts/validate-policies.sh&lt;/code&gt; — calls Access Analyzer &lt;code&gt;ValidatePolicy&lt;/code&gt; for both documents.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;scripts/simulate-policies.sh&lt;/code&gt; — evaluates three separate action/resource cases.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;scripts/capture-evidence.sh&lt;/code&gt; — runs the checks and stores fresh command outputs.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;scripts/run-local-checks.sh&lt;/code&gt; — runs the local checks, if included in the project version.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;scripts/cleanup.sh&lt;/code&gt; — provides local evidence cleanup guidance.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. Stage 1: Local static checks
&lt;/h2&gt;

&lt;p&gt;The Python program parses the JSON document and inspects selected statement fields, including &lt;code&gt;Effect&lt;/code&gt;, &lt;code&gt;Action&lt;/code&gt;, and &lt;code&gt;Resource&lt;/code&gt;. Its rules include patterns such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Allow&lt;/code&gt; with both &lt;code&gt;Action: "*"&lt;/code&gt; and &lt;code&gt;Resource: "*"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A service wildcard such as &lt;code&gt;s3:*&lt;/code&gt; paired with &lt;code&gt;Resource: "*"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Selected sensitive IAM actions allowed on &lt;code&gt;Resource: "*"&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Run the checker from the project root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./scripts/audit-policies.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The deliberately over-permissive sample is expected to produce findings. The scoped sample is expected to pass this checker's limited rule set. A clean result means only that none of the implemented rules matched; it is not a full IAM security assessment. The checker does not infer application requirements or evaluate every IAM policy feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Stage 2: AWS policy validation
&lt;/h2&gt;

&lt;p&gt;Run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./scripts/validate-policies.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script calls the Access Analyzer &lt;code&gt;ValidatePolicy&lt;/code&gt; API for both policy documents. A representative command is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws accessanalyzer validate-policy &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--policy-document&lt;/span&gt; file://policies/least-privilege-policy.json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--policy-type&lt;/span&gt; IDENTITY_POLICY &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Review the actual &lt;code&gt;findings&lt;/code&gt; returned by AWS. Finding types and messages are generated by AWS. Do not assume that every insecure-looking policy will return one particular finding, or that an empty findings list proves the policy is safe.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Stage 3: IAM Policy Simulator
&lt;/h2&gt;

&lt;p&gt;The scoped policy is intended to allow &lt;code&gt;s3:PutObject&lt;/code&gt; only for objects under the fictional bucket's &lt;code&gt;telemetry/&lt;/code&gt; prefix. It includes an explicit restriction for bucket deletion and does not grant object deletion.&lt;/p&gt;

&lt;p&gt;Run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./scripts/simulate-policies.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script makes three separate simulator calls so each action is evaluated against its corresponding resource ARN.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;th&gt;Resource&lt;/th&gt;
&lt;th&gt;Expected decision from the supplied policy&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Intended operation&lt;/td&gt;
&lt;td&gt;&lt;code&gt;s3:PutObject&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;arn:aws:s3:::article05-demo-bucket/telemetry/sample.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;allowed&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Explicit restriction&lt;/td&gt;
&lt;td&gt;&lt;code&gt;s3:DeleteBucket&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;arn:aws:s3:::article05-demo-bucket&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;explicitDeny&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Not granted&lt;/td&gt;
&lt;td&gt;&lt;code&gt;s3:DeleteObject&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;arn:aws:s3:::article05-demo-bucket/telemetry/sample.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;implicitDeny&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These are expected outcomes based on the sample policy and simulator inputs. Use the actual simulator output as the source of truth for the run and update the article if it differs.&lt;/p&gt;

&lt;p&gt;The simulator does not send a real S3 request and does not prove that a real request would succeed. Actual authorization can depend on additional identity, resource, permissions-boundary, session, Organizations, and request-context controls.&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyd097sp5wcx80bavdwn9.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyd097sp5wcx80bavdwn9.png" alt=" " width="800" height="662"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Evidence collection
&lt;/h2&gt;

&lt;p&gt;Run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./scripts/capture-evidence.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fresh command output is saved under &lt;code&gt;evidence/results/&lt;/code&gt;. The capture script stores the local auditor output, Access Analyzer responses, and simulator decisions.&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Local Python auditor findings for the over-permissive policy.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffmjwmhvwxcty7t9j7m33.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffmjwmhvwxcty7t9j7m33.png" alt=" " width="800" height="129"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Access Analyzer findings for the over-permissive policy.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy8a118zi9dyyl6zjfm4v.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy8a118zi9dyyl6zjfm4v.png" alt=" " width="800" height="393"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;3. Access Analyzer response for the scoped policy.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fblz5p583395cpgjxi7m8.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fblz5p583395cpgjxi7m8.png" alt=" " width="800" height="141"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;4. Simulator output for the three action/resource cases.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwfep4eye3ljqu8qvytob.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwfep4eye3ljqu8qvytob.png" alt=" " width="800" height="662"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Cost and safety considerations
&lt;/h2&gt;

&lt;p&gt;This workflow does not provision AWS infrastructure, attach policies, or issue S3 data-plane requests. It invokes IAM policy validation and simulation APIs. Check current AWS pricing and your own account billing separately; this project does not guarantee that unrelated account activity has no cost.&lt;/p&gt;

&lt;p&gt;Do not attach the over-permissive sample to a real IAM user, group, or role. The bucket ARN is fictional; no bucket is created or contacted.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Limitations and future work
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Expand the local checker's test suite and rule coverage while keeping its limitations explicit.&lt;/li&gt;
&lt;li&gt;Add unit tests for malformed documents, single-statement and array statements, wildcard actions, and sensitive permissions.&lt;/li&gt;
&lt;li&gt;Integrate the checks into a pull-request workflow after reviewing false-positive and failure-handling behavior.&lt;/li&gt;
&lt;li&gt;Add workload-specific requirements so a policy can be reviewed against what the application actually needs.&lt;/li&gt;
&lt;li&gt;Evaluate permissions boundaries, resource policies, session policies, and Organizations controls in an appropriate review or simulation context.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Least privilege is not achieved by finding one wildcard and replacing it mechanically. It requires understanding the workload's intended actions and resources, checking policy structure, and testing representative authorization cases. A local checker, Access Analyzer validation, and Policy Simulator provide three useful review layers before a policy is deployed—without creating test infrastructure for this demonstration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repository:&lt;/strong&gt; [ &lt;a href="https://github.com/narendra-sre/aws-community-builders/tree/main/article-05-iam-policy-security" rel="noopener noreferrer"&gt;AWS Community Builder Repository&lt;/a&gt;]&lt;/p&gt;

</description>
      <category>iam</category>
      <category>security</category>
      <category>terraform</category>
      <category>aws</category>
    </item>
    <item>
      <title>Preventing Public Exposure Before It Happens: Event-Driven AWS Security Guardrails with Terraform, Lambda, and EventBridge</title>
      <dc:creator>Narendra | SRE</dc:creator>
      <pubDate>Thu, 05 Nov 2026 16:18:11 +0000</pubDate>
      <link>https://dev.to/narendrasre/preventing-public-exposure-before-it-happens-event-driven-aws-security-guardrails-with-terraform-4ekm</link>
      <guid>https://dev.to/narendrasre/preventing-public-exposure-before-it-happens-event-driven-aws-security-guardrails-with-terraform-4ekm</guid>
      <description>&lt;p&gt;&amp;nbsp;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Implementation Status&lt;/strong&gt;: The guardrail was deployed in AWS and validated using EventBridge, Lambda, CloudTrail, EC2 Security Groups, S3, DynamoDB, and SNS.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;p&gt;Most cloud security tooling tells you about a mistake after it has already happened. A scanner runs on a schedule, a finding appears in a dashboard, and someone eventually opens a ticket. In the meantime, a security group that allows SSH from &lt;code&gt;0.0.0.0/0&lt;/code&gt; is reachable from the entire internet, and a bucket whose public access block has been removed can be opened to the internet with a single ACL or bucket policy change.&lt;/p&gt;

&lt;p&gt;In this article I build a guardrail that reacts to the API call itself rather than to the next scan. When someone opens a sensitive port to the world or weakens the public access block on an S3 bucket, a Lambda function is invoked within seconds. It either reverts the change or, in dry-run mode, only alerts. Every decision is written to an audit table, and the team receives an email. The entire stack is provisioned with Terraform and designed to run inside the AWS Free Tier.&lt;/p&gt;

&lt;p&gt;The complete project, including Terraform code, the remediator, test scripts, and sanitized evidence, is in my GitHub repository, linked at the end.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The Problem
&lt;/h2&gt;

&lt;p&gt;Two misconfigurations cause a disproportionate share of avoidable cloud exposure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Administrative ports open to the internet.&lt;/strong&gt; SSH (22) and RDP (3389) opened to &lt;code&gt;0.0.0.0/0&lt;/code&gt; are routinely targeted by automated scanners within minutes of appearing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Public S3 buckets.&lt;/strong&gt; Buckets become public when the Block Public Access settings are removed, usually to make a quick test work and then forgotten.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both are easy to introduce. A single CLI command or console click is enough. Both are also easy to miss: the change looks reasonable in isolation, and nobody is watching the API stream.&lt;/p&gt;

&lt;p&gt;Detection alone has a structural weakness. A scheduled scan has a detection window measured in hours, while the change itself takes effect in seconds. Auto-remediation closes that window, but it introduces its own risk. A guardrail that silently breaks a legitimate workflow is worse than none. The design therefore has to answer three questions: how to act quickly, how to avoid acting wrongly, and how to prove afterwards what happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Architecture
&lt;/h2&gt;

&lt;p&gt;The flow has six steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;An engineer or an automation pipeline calls &lt;code&gt;AuthorizeSecurityGroupIngress&lt;/code&gt;, &lt;code&gt;DeleteBucketPublicAccessBlock&lt;/code&gt;, or &lt;code&gt;PutBucketPublicAccessBlock&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;CloudTrail records the call. Its management events reach the default EventBridge bus as &lt;code&gt;AWS API Call via CloudTrail&lt;/code&gt;, so no additional trail is required for detection.&lt;/li&gt;
&lt;li&gt;An EventBridge rule matches only the three violation-relevant event names, and invokes the remediator Lambda with up to three retries.&lt;/li&gt;
&lt;li&gt;The Lambda claims the event ID in DynamoDB, evaluates the change, and either remediates or simulates the remediation depending on the dry-run setting.&lt;/li&gt;
&lt;li&gt;The outcome is written to the findings table, and an alert is published to SNS and delivered by email.&lt;/li&gt;
&lt;li&gt;Any event that fails every retry is parked in an SQS dead-letter queue for inspection.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgvtmvg5tzm4hn8x4sk27.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgvtmvg5tzm4hn8x4sk27.png" alt=" " width="800" height="317"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The services were chosen for cost as much as capability. I avoided AWS Config and Security Hub, which are powerful but billed per rule evaluation or finding, and I avoided customer-managed KMS keys, which carry a monthly charge. Lambda, EventBridge rules on the default bus, DynamoDB on-demand, SNS email, SQS, and CloudWatch Logs with 14-day retention all fit within the Free Tier for a workload of this size.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Design Decisions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Event-driven rather than polling.&lt;/strong&gt; The guardrail reacts to the API call, so it acts within seconds and does not pay for periodic full-account scans.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Revoke the exact rule, narrowed to the world.&lt;/strong&gt; The remediator does not wipe the security group. It removes only the authorized rule, and only the &lt;code&gt;0.0.0.0/0&lt;/code&gt; or &lt;code&gt;::/0&lt;/code&gt; entries within it. A rule that also allows a private CIDR keeps its private entries. This keeps the blast radius of the correction as small as the violation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Idempotent processing.&lt;/strong&gt; EventBridge delivers at least once, so the same event can arrive twice. The remediator writes the event ID to DynamoDB with a conditional &lt;code&gt;PutItem&lt;/code&gt;. A second delivery fails the condition and is recorded as a duplicate. If processing crashes midway, the claim is deleted before the error is re-raised, so the retry can run to completion instead of being wrongly skipped.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Loop safety.&lt;/strong&gt; Remediation itself produces CloudTrail events. Revoking an ingress rule emits &lt;code&gt;RevokeSecurityGroupIngress&lt;/code&gt;, which the rule does not match. Restoring a public access block emits &lt;code&gt;PutBucketPublicAccessBlock&lt;/code&gt;, which the rule does match, but the handler first checks compliance and does nothing if the bucket is already fully blocked. The system converges instead of looping.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dry-run by default.&lt;/strong&gt; The first deployment sets &lt;code&gt;DRY_RUN=true&lt;/code&gt;. It records what it would have changed and sends alerts, but changes nothing. This lets a team observe real traffic for days before enforcing. Enforcement is a single Terraform variable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Exemptions by tag.&lt;/strong&gt; Some resources legitimately need public access, such as a static website bucket or a bastion host. A resource tagged &lt;code&gt;guardrail-exempt=true&lt;/code&gt; is skipped and recorded as exempt. The tradeoff is that anyone who can apply tags can grant an exemption, so tag-write permissions become a security-sensitive privilege. I discuss how to govern that in the limitations section.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Least privilege.&lt;/strong&gt; Each IAM statement maps to a specific API call in the handler. Describe calls are the only ones that cannot be scoped by resource, and they are isolated in their own statement. The one known gap is the S3 statement, which covers all buckets. Narrowing it with a tag condition is planned future work, and I've documented it in the code rather than hiding it.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Implementation
&lt;/h2&gt;

&lt;h3&gt;
  
  
  4.1 Detection rule
&lt;/h3&gt;

&lt;p&gt;The EventBridge pattern matches the CloudTrail event envelope on source service and event name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;event_pattern&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonencode&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="s2"&gt;"detail-type"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"AWS API Call via CloudTrail"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;detail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;eventSource&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"ec2.amazonaws.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"s3.amazonaws.com"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;eventName&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s2"&gt;"AuthorizeSecurityGroupIngress"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bucket_events&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="err"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The target carries both a retry policy and a dead-letter queue. These two settings are what turn "the Lambda failed once" into "the event was retried for an hour and then preserved for inspection."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;retry_policy&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;maximum_event_age_in_seconds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;
  &lt;span class="nx"&gt;maximum_retry_attempts&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;dead_letter_config&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;arn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_sqs_queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dlq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4.2 Revoking only the violating rule
&lt;/h3&gt;

&lt;p&gt;The security group handler inspects each authorized permission in the CloudTrail record and builds the smallest revoke request that undoes the violation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_revoke_rule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;perm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v6&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;rule&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;IpProtocol&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;perm&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ipProtocol&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fromPort&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;perm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FromPort&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;perm&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fromPort&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ToPort&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;perm&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;toPort&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;v4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;IpRanges&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CidrIp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cidr&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;cidr&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;v4&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;v6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Ipv6Ranges&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CidrIpv6&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cidr&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;cidr&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;v6&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;rule&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The revoke must match the original protocol and port range exactly, which is why the handler copies them from the event rather than reconstructing them from the configured sensitive ports.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.3 Claiming each event once
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_claim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;findings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put_item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;Item&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PROCESSING&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="n"&gt;ConditionExpression&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;attribute_not_exists(event_id)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;ClientError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ConditionalCheckFailedException&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The wrapper that calls this function releases the claim on failure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;outcome&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;findings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete_item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without that release, a transient API error would leave the event permanently marked as processing, and every retry would be skipped.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.4 Infrastructure
&lt;/h3&gt;

&lt;p&gt;Everything is defined in five Terraform files: &lt;code&gt;versions.tf&lt;/code&gt; pins the AWS and archive providers, &lt;code&gt;provider.tf&lt;/code&gt; applies default tags, &lt;code&gt;variables.tf&lt;/code&gt; exposes the dry-run switch, sensitive port list, and alert address, &lt;code&gt;main.tf&lt;/code&gt; declares the resources, and &lt;code&gt;outputs.tf&lt;/code&gt; exposes names needed by the test and evidence scripts. The handler is packaged from &lt;code&gt;lambda/handler.py&lt;/code&gt; by an archive data source, so a code change produces a new &lt;code&gt;source_code_hash&lt;/code&gt; and Terraform updates the function on the next apply.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Testing the Guardrail
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;scripts/simulate-violations.sh&lt;/code&gt; exercises three scenarios:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open SSH on a normal security group. This should be a violation.&lt;/li&gt;
&lt;li&gt;Open SSH on a security group tagged &lt;code&gt;guardrail-exempt=true&lt;/code&gt;. This should be allowed.&lt;/li&gt;
&lt;li&gt;Delete the public access block on a new bucket. This should be a violation.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I ran the test in dry-run mode first, confirmed that the alerts and audit records were correct and nothing was changed, and then set &lt;code&gt;dry_run = false&lt;/code&gt; and reran it.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Expected in dry-run&lt;/th&gt;
&lt;th&gt;Expected in enforce mode&lt;/th&gt;
&lt;th&gt;Observed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1. Open SSH, untagged SG&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;would_revoke&lt;/code&gt; recorded, alert sent, rule still present&lt;/td&gt;
&lt;td&gt;rule revoked, alert sent&lt;/td&gt;
&lt;td&gt;&lt;br&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqj2udlmgptb1x109hjoj.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqj2udlmgptb1x109hjoj.png" alt=" " width="800" height="359"&gt;&lt;/a&gt;&lt;br&gt;&lt;br&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcxx9fyjg94vx7jgn42uf.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcxx9fyjg94vx7jgn42uf.png" alt=" " width="800" height="360"&gt;&lt;/a&gt; |&lt;br&gt;
| 2. Open SSH, exempt SG         | &lt;code&gt;none&lt;/code&gt; (exempt), no alert                               | same                       | &lt;br&gt;&lt;br&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flbi7nbxx929y14f5z7a8.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flbi7nbxx929y14f5z7a8.png" alt=" " width="800" height="359"&gt;&lt;/a&gt;                                                                                                                  |&lt;br&gt;
| 3. Public access block deleted | &lt;code&gt;would_restore&lt;/code&gt; recorded, alert sent                    | block restored, alert sent | &lt;br&gt;&lt;br&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fudktbfj384rxvygwtwrh.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fudktbfj384rxvygwtwrh.png" alt=" " width="800" height="392"&gt;&lt;/a&gt;                                                                                                         |&lt;/p&gt;

&lt;p&gt;The time from API call to revocation was [insert measured seconds from CloudTrail eventTime to the revoke call in the logs]. This is the window in which the exposure existed, and it is the number that matters most when comparing this approach with scheduled scanning.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Evidence
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;evidence/&lt;/code&gt; folder contains sanitized outputs from the run. Account IDs are replaced with &lt;code&gt;111122223333&lt;/code&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;What it shows&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;terraform-plan-summary.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The resources the stack creates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;remediator-logs-sanitized.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Lambda decisions, including duplicate and exempt paths&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cloudtrail-authorize-ingress-sanitized.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The originating API call for scenario 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cloudtrail-delete-public-access-block-sanitized.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The originating API call for scenario 3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sg-after-remediation-sanitized.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The security group after enforcement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;screenshots/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Console views of the alert email, DynamoDB items, and bucket settings&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  7. Cost
&lt;/h2&gt;

&lt;p&gt;On an account used only for this project, the workload should stay within the Free Tier: a handful of Lambda invocations, a few dozen DynamoDB writes, a few email notifications, and a small amount of log data. I verified this against the Billing dashboard after running the simulation, and the observed charges were [insert observed charges, or "zero"]. Costs can change, so check your own account's Free Tier usage before leaving the stack running.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Limitations and Future Work
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The guardrail is reactive, not preventive.&lt;/strong&gt; There is a window of seconds between the API call and the correction. A Service Control Policy can prevent some of these calls outright, and I would layer one on top in a production setting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It does not find existing exposure.&lt;/strong&gt; The guardrail only sees new API calls. A one-time sweep of existing security groups and buckets would complement it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Exemptions need governance.&lt;/strong&gt; Because exemption is a tag, the control is only as strong as the permission to write tags. A production version should restrict tag changes on guarded resources, or require an approval record.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bucket policies and ACLs are out of scope.&lt;/strong&gt; This rule covers the public access block only. A bucket made public through a bucket policy or ACL would need additional rules.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The S3 IAM statement is broader than ideal.&lt;/strong&gt; It covers all buckets. A tag-based condition would narrow it to buckets the team owns.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Public exposure is one of the most common and most preventable cloud misconfigurations. Scanning finds it eventually, but the exposure starts the moment the API call succeeds. This project closes that gap with an event-driven guardrail that acts within seconds, revokes only what violates policy, records every decision, and fails safely.&lt;/p&gt;

&lt;p&gt;The design choices that matter most are not the services but the behaviors around them: dry-run before enforcement, idempotent processing, a dead-letter queue, narrowly scoped revocation, and an explicit exemption path. Those choices are what make an automated security control safe to run in a real account.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub repository:&lt;/strong&gt; [&lt;a href="https://github.com/narendra-sre/aws-community-builders/tree/main/article-04-serverless-security-guardrails" rel="noopener noreferrer"&gt;AWS-Community-Builder Repository&lt;/a&gt;]&lt;/p&gt;

</description>
      <category>terraform</category>
      <category>awslambda</category>
      <category>amazoncloudtrail</category>
      <category>amazoneventbridge</category>
    </item>
    <item>
      <title>Detecting and Remediating AWS Infrastructure Drift Using Terraform and CloudTrail</title>
      <dc:creator>Narendra | SRE</dc:creator>
      <pubDate>Wed, 04 Nov 2026 14:14:17 +0000</pubDate>
      <link>https://dev.to/narendrasre/detecting-and-remediating-aws-infrastructure-drift-using-terraform-and-cloudtrail-4mkf</link>
      <guid>https://dev.to/narendrasre/detecting-and-remediating-aws-infrastructure-drift-using-terraform-and-cloudtrail-4mkf</guid>
      <description>&lt;p&gt;&amp;nbsp;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Implementation Status: The demonstration was deployed and validated using AWS EC2, CloudTrail and Security groups.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;p&gt;Infrastructure as Code (IaC) has become an important part of modern cloud infrastructure management. Tools such as Terraform allow engineering teams to provision, configure, and manage cloud resources consistently through declarative configuration files.&lt;/p&gt;

&lt;p&gt;However, managing infrastructure through code does not guarantee that every change will always happen through code.&lt;/p&gt;

&lt;p&gt;In real-world environments, administrators or engineers may manually modify AWS resources through the AWS Management Console or AWS CLI. These changes can cause the actual infrastructure to differ from the configuration maintained in Terraform.&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;

&lt;p&gt;This situation is known as &lt;strong&gt;infrastructure drift&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;

&lt;p&gt;In this article, I'll demonstrate how to introduce infrastructure drift deliberately in AWS, detect it using Terraform, investigate the manual modification using AWS CloudTrail, and restore the desired configuration using Terraform.&lt;/p&gt;

&lt;p&gt;We'll use an Amazon EC2 instance and its tags as a simple but practical example.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Understanding Infrastructure Drift
&lt;/h2&gt;

&lt;p&gt;Infrastructure drift occurs when the actual configuration of a cloud resource differs from the desired configuration defined in Infrastructure as Code.&lt;/p&gt;

&lt;p&gt;Consider an EC2 instance managed through Terraform with the following tags:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&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="s2"&gt;"Terraform infrastructure drift"&lt;/span&gt;
  &lt;span class="nx"&gt;Environment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"development"&lt;/span&gt;
  &lt;span class="nx"&gt;ManagedBy&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Terraform"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now imagine an administrator manually changes the Environment tag through the AWS Console:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Environment = production
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The actual AWS resource now differs from the Terraform configuration.&lt;/p&gt;

&lt;p&gt;Terraform still declares:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Environment = development
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But AWS reports:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Environment = production
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This difference is infrastructure drift.&lt;/p&gt;

&lt;p&gt;Drift can create configuration inconsistencies, unexpected infrastructure behavior, security risks, and difficulties during infrastructure maintenance.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Architecture and Technology Stack
&lt;/h2&gt;

&lt;p&gt;The demonstration uses the following components:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Terraform: Infrastructure provisioning, drift detection, and remediation.&lt;/li&gt;
&lt;li&gt;Amazon EC2: The AWS resource used for the drift experiment.&lt;/li&gt;
&lt;li&gt;Amazon VPC security group: Controls network access to the EC2 instance.&lt;/li&gt;
&lt;li&gt;AWS CloudTrail: Records the API activity associated with the manual change.&lt;/li&gt;
&lt;li&gt;AWS CLI: Inspects AWS resources and retrieves audit events.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The implementation is performed in the AWS Asia Pacific (Hyderabad) Region (&lt;code&gt;ap-south-2&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Architecture flow:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Terraform provisions an EC2 instance. A manual modification changes its Environment tag. Terraform detects the difference, CloudTrail provides an audit record of the modification, and Terraform restores the declared configuration.&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;br&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F60lsma2cfjshq1apc6sc.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F60lsma2cfjshq1apc6sc.png" alt=" " width="799" height="372"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  3. Provisioning Infrastructure Using Terraform
&lt;/h2&gt;

&lt;p&gt;The first step is to define the required AWS infrastructure using Terraform.&lt;/p&gt;

&lt;p&gt;The demonstration provisions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One Amazon EC2 instance.&lt;/li&gt;
&lt;li&gt;One security group.&lt;/li&gt;
&lt;li&gt;An Amazon Linux 2023 AMI discovered dynamically through a Terraform data source.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The EC2 instance uses the &lt;code&gt;t3.micro&lt;/code&gt; instance type.&lt;/p&gt;
&lt;h3&gt;
  
  
  AMI discovery
&lt;/h3&gt;

&lt;p&gt;Instead of hardcoding an AMI ID, Terraform uses an AWS data source to discover a suitable Amazon Linux 2023 image.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_ami"&lt;/span&gt; &lt;span class="s2"&gt;"amazon_linux"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;most_recent&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;owners&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"amazon"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="nx"&gt;filter&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="s2"&gt;"name"&lt;/span&gt;
    &lt;span class="nx"&gt;values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"al2023-ami-2023.*-x86_64"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;filter&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="s2"&gt;"architecture"&lt;/span&gt;
    &lt;span class="nx"&gt;values&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"x86_64"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;filter&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="s2"&gt;"virtualization-type"&lt;/span&gt;
    &lt;span class="nx"&gt;values&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"hvm"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  EC2 resource
&lt;/h3&gt;

&lt;p&gt;The EC2 instance is configured with the discovered AMI and the selected instance type.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_instance"&lt;/span&gt; &lt;span class="s2"&gt;"drift_demo"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;ami&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_ami&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amazon_linux&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;instance_type&lt;/span&gt;          &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;instance_type&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_security_group_ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_security_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;drift_demo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&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="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project_name&lt;/span&gt;
    &lt;span class="nx"&gt;Environment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"development"&lt;/span&gt;
    &lt;span class="nx"&gt;ManagedBy&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Terraform"&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;p&gt;The Environment tag is important because we'll intentionally modify it outside Terraform.&lt;/p&gt;

&lt;h3&gt;
  
  
  Terraform initialization and validation
&lt;/h3&gt;

&lt;p&gt;Before provisioning, I initialized and validated the Terraform configuration.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;terraform&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;init&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;terraform&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;fmt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-recursive&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;terraform&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;validate&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I generated an execution plan:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;terraform&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-out&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tfplan&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After reviewing the plan, I deployed the infrastructure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;terraform&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;apply&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;tfplan&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The deployment created the EC2 instance and security group.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Establishing the Initial Infrastructure State
&lt;/h2&gt;

&lt;p&gt;Before introducing drift, I verified the actual AWS resource configuration.&lt;/p&gt;

&lt;p&gt;I retrieved the EC2 instance ID:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$instanceId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;terraform&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-raw&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;instance_id&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I then inspected the EC2 tags using AWS CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ec2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;describe-tags&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--filters&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Name=resource-id,Values=&lt;/span&gt;&lt;span class="nv"&gt;$instanceId&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--region&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ap-south-2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--query&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Tags[].{Key:Key,Value:Value}"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--output&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The initial configuration was:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tag&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Name&lt;/td&gt;
&lt;td&gt;Terraform infrastructure drift&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Environment&lt;/td&gt;
&lt;td&gt;development&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ManagedBy&lt;/td&gt;
&lt;td&gt;Terraform&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I also ran:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;terraform&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Terraform reported:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;No changes. Your infrastructure matches the configuration.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This established our baseline: the actual AWS infrastructure matched the declared Terraform configuration.&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;br&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftiw5qqwg7hn4yzu74z4y.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftiw5qqwg7hn4yzu74z4y.png" alt=" " width="800" height="362"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  5. Introducing Infrastructure Drift
&lt;/h2&gt;

&lt;p&gt;To simulate a real-world manual infrastructure change, I modified the Environment tag directly through the AWS Management Console.&lt;/p&gt;

&lt;p&gt;I navigated to:&lt;/p&gt;

&lt;p&gt;EC2 → Instances → Select instance → Tags → Manage tags.&lt;/p&gt;

&lt;p&gt;I changed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Environment = development
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Environment = production
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I did not modify the Terraform configuration.&lt;/p&gt;

&lt;p&gt;This is important: the difference was introduced directly in AWS, not through Terraform.&lt;/p&gt;

&lt;p&gt;I verified the updated tag using AWS CLI.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ec2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;describe-tags&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--filters&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Name=resource-id,Values=&lt;/span&gt;&lt;span class="nv"&gt;$instanceId&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--region&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ap-south-2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--query&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Tags[].{Key:Key,Value:Value}"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--output&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The actual AWS configuration now showed:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tag&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Name&lt;/td&gt;
&lt;td&gt;Terraform infrastructure drift&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Environment&lt;/td&gt;
&lt;td&gt;production&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ManagedBy&lt;/td&gt;
&lt;td&gt;Terraform&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The Terraform configuration still declared &lt;code&gt;Environment = development&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We had successfully introduced infrastructure drift.&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;br&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhftbvy489osabxk4axc5.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhftbvy489osabxk4axc5.png" alt=" " width="800" height="362"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  6. Detecting Infrastructure Drift Using Terraform
&lt;/h2&gt;

&lt;p&gt;The next step was to determine whether Terraform could identify the difference.&lt;/p&gt;

&lt;p&gt;I executed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;terraform&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Terraform refreshed its view of the managed resource and compared the observed infrastructure with the declared configuration.&lt;/p&gt;

&lt;p&gt;The plan identified an in-place update to the EC2 instance tags.&lt;/p&gt;

&lt;p&gt;The relevant difference was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Environment = production -&amp;gt; development
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;~&lt;/code&gt; symbol in Terraform's plan output indicates that the resource is being updated in place.&lt;/p&gt;

&lt;p&gt;To preserve the execution plan, I generated a saved plan:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;terraform&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-out&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;drift-plan&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also exported the plan in JSON format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;terraform&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;show&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-json&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;drift-plan&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;Out-File&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Encoding&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;utf8&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="nx"&gt;\evidence\terraform-plan-drift.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This demonstrated that Terraform had detected the difference between the actual AWS resource and the desired configuration.&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;br&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd5xhsrzolsmowm5cnyey.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd5xhsrzolsmowm5cnyey.png" alt=" " width="800" height="368"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  7. Investigating Manual Changes Using AWS CloudTrail
&lt;/h2&gt;

&lt;p&gt;Detecting drift is important, but understanding how the change occurred is equally important.&lt;/p&gt;

&lt;p&gt;For this, I used AWS CloudTrail.&lt;/p&gt;

&lt;p&gt;When EC2 tags are created or modified through AWS APIs, CloudTrail records the &lt;code&gt;CreateTags&lt;/code&gt; management event.&lt;/p&gt;

&lt;p&gt;I queried the CloudTrail event history:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cloudtrail&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;lookup-events&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--lookup-attributes&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;AttributeKey&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;EventName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;AttributeValue&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;CreateTags&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--max-results&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;50&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--region&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ap-south-2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--output&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I inspected the event associated with the EC2 instance.&lt;/p&gt;

&lt;p&gt;The event provides information such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Event name and time.&lt;/li&gt;
&lt;li&gt;AWS Region.&lt;/li&gt;
&lt;li&gt;Identity that performed the operation.&lt;/li&gt;
&lt;li&gt;Resource affected.&lt;/li&gt;
&lt;li&gt;Requested tag values.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The CloudTrail record provided an audit trail for the manual infrastructure modification.&lt;/p&gt;

&lt;p&gt;By comparing the initial EC2 tag evidence with the CloudTrail event and the drifted configuration, I could establish what changed and investigate the corresponding API activity.&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;br&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fayyqu5xhzlllb596ze29.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fayyqu5xhzlllb596ze29.png" alt=" " width="800" height="338"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  8. Remediating Infrastructure Drift
&lt;/h2&gt;

&lt;p&gt;After identifying the difference, I reviewed the Terraform execution plan.&lt;/p&gt;

&lt;p&gt;The proposed change was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Environment = production -&amp;gt; development
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since the desired configuration was &lt;code&gt;development&lt;/code&gt;, I proceeded with the reviewed plan.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;terraform&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;apply&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;drift-plan&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Terraform applied the proposed update and restored the Environment tag.&lt;/p&gt;

&lt;p&gt;I verified the result using AWS CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ec2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;describe-tags&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--filters&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Name=resource-id,Values=&lt;/span&gt;&lt;span class="nv"&gt;$instanceId&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--region&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ap-south-2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--query&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Tags[].{Key:Key,Value:Value}"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--output&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Environment tag was restored to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Environment = development
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The EC2 instance itself was not replaced. The correction was an in-place tag update.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Final Validation
&lt;/h2&gt;

&lt;p&gt;To verify that the infrastructure was back in the desired state, I executed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;terraform&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Terraform reported:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;No changes. Your infrastructure matches the configuration.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This confirmed that the observed AWS configuration matched the Terraform configuration after remediation.&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;br&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuqxu7myqwv99opzjjham.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuqxu7myqwv99opzjjham.png" alt=" " width="798" height="119"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Key Learnings
&lt;/h2&gt;

&lt;p&gt;Through this implementation, I gained practical experience with several important infrastructure management concepts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Infrastructure drift&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Manual changes outside Infrastructure as Code can cause differences between desired and actual infrastructure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Terraform state and refresh&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Terraform uses its state and refresh behavior to understand managed resources and compare them with the declared configuration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Drift detection&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;terraform plan&lt;/code&gt; helps identify differences and presents proposed changes for review.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. CloudTrail auditing&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;CloudTrail provides an audit trail of AWS API activity, helping engineers investigate infrastructure modifications.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Controlled remediation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Terraform does not automatically correct every detected difference. Engineers must review the proposed changes and explicitly apply them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Infrastructure governance&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Teams should establish clear processes for infrastructure changes and minimize unmanaged manual modifications.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Infrastructure drift is a common challenge in cloud environments where infrastructure can be modified through multiple access paths.&lt;/p&gt;

&lt;p&gt;In this demonstration, I provisioned an EC2 instance using Terraform, deliberately modified its tag through the AWS Console, detected the resulting drift using Terraform, investigated the API activity using AWS CloudTrail, and restored the desired configuration.&lt;/p&gt;

&lt;p&gt;The final Terraform plan confirmed that the infrastructure matched the declared configuration again.&lt;/p&gt;

&lt;p&gt;This exercise demonstrates how Terraform and CloudTrail can complement each other: Terraform helps identify and correct configuration differences, while CloudTrail provides an audit trail for investigating AWS API activity.&lt;/p&gt;

&lt;p&gt;The complete Terraform implementation and sanitized evidence are available in my GitHub repository.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub repository:&lt;/strong&gt; [&lt;a href="https://github.com/narendra-sre/aws-community-builders/tree/main/article-03-terraform-infrastructure-drift" rel="noopener noreferrer"&gt;AWS Community Builders repository&lt;/a&gt;]&lt;/p&gt;




</description>
      <category>amazonec2</category>
      <category>amazoncloudtrail</category>
      <category>terraform</category>
    </item>
    <item>
      <title>Secure AWS Automation with GitHub Actions and OpenID Connect (OIDC)</title>
      <dc:creator>Narendra | SRE</dc:creator>
      <pubDate>Sat, 31 Oct 2026 14:47:51 +0000</pubDate>
      <link>https://dev.to/narendrasre/secure-aws-automation-with-github-actions-and-openid-connect-oidc-1kd2</link>
      <guid>https://dev.to/narendrasre/secure-aws-automation-with-github-actions-and-openid-connect-oidc-1kd2</guid>
      <description>&lt;h1&gt;
  
  
  Secure AWS Automation with GitHub Actions and OpenID Connect (OIDC)
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Level:&lt;/strong&gt; Intermediate&lt;br&gt;
&lt;strong&gt;Focus:&lt;/strong&gt; AWS IAM, temporary credentials, OpenID Connect, GitHub Actions, least privilege.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example code:&lt;/strong&gt; &lt;a href="https://github.com/narendra-sre/aws-community-builders/tree/main/article-02-secure-aws-automation-oidc" rel="noopener noreferrer"&gt;aws-community-builders repository&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Implementation status:&lt;/strong&gt;&lt;br&gt;
Created the GitHub Actions OIDC identity provider in AWS IAM.&lt;br&gt;
Created a dedicated S3 test bucket.&lt;br&gt;
Configured an IAM role with an exact GitHub OIDC subject condition.&lt;br&gt;
Configured short-lived AWS credentials using GitHub Actions.&lt;br&gt;
Successfully assumed the IAM role through OIDC.&lt;br&gt;
Successfully accessed the permitted reports/ prefix.&lt;br&gt;
Verified that access to private/ was denied.&lt;br&gt;
Captured Terraform, IAM, CloudTrail and GitHub Actions evidence.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;CI/CD pipelines often need access to cloud resources: publishing an artifact, deploying an application, or updating infrastructure. A common shortcut is to create an IAM user's access key and store it as a CI/CD secret. These credentials are reusable until rotated or revoked. If exposed through a compromised runner, workflow, or accidental logging, they may be used outside the pipeline.&lt;/p&gt;

&lt;p&gt;AWS supports a different pattern: a workload proves its identity using OpenID Connect (OIDC), then requests temporary AWS credentials for an IAM role. This article explains that pattern using GitHub Actions and AWS Security Token Service (AWS STS).&lt;/p&gt;

&lt;p&gt;The objective is not just to remove access keys. It is to establish a narrowly scoped trust relationship between a specific repository and workflow context and one AWS role, then grant that role only the permissions the workflow needs.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fprod-assets.cosmic.aws.dev%2Fa%2F3LSmkEPEXMlb1fwKVJYddDH7gXY%2F01-o.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fprod-assets.cosmic.aws.dev%2Fa%2F3LSmkEPEXMlb1fwKVJYddDH7gXY%2F01-o.webp" title="Architecture" alt="GitHub Actions OIDC architecture" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 1. GitHub Actions obtains a short-lived role session through OIDC instead of using a stored long-lived AWS access key.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  1. Why long-lived access keys are risky
&lt;/h2&gt;

&lt;p&gt;Static IAM user access keys can remain valid for a long time. They may be copied into workflows, developer machines, or secret stores. Rotation becomes an operational task, and every copy must be updated safely.&lt;/p&gt;

&lt;p&gt;Common risks include accidental commits, credentials printed by workflows, compromised runners, keys remaining active after a project ends, and broad permissions increasing the impact of exposure.&lt;/p&gt;

&lt;p&gt;Secret storage is important for credentials that must exist, but it does not change the fact that a static access key is reusable. Where workload federation is supported, short-lived credentials are usually a better fit.&lt;/p&gt;
&lt;h2&gt;
  
  
  2. The building blocks
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GitHub Actions&lt;/td&gt;
&lt;td&gt;Runs the automation workflow.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub OIDC issuer&lt;/td&gt;
&lt;td&gt;Issues a signed identity token for the workflow context.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS IAM OIDC provider&lt;/td&gt;
&lt;td&gt;Establishes trust in the token issuer and audience.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IAM role trust policy&lt;/td&gt;
&lt;td&gt;Defines which federated identities may assume the role.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS STS&lt;/td&gt;
&lt;td&gt;Validates the token and issues temporary credentials.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IAM permissions policy&lt;/td&gt;
&lt;td&gt;Defines what the assumed role can do in AWS.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two policy types must be understood separately:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Trust policy:&lt;/strong&gt; Who can assume this role?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Permissions policy:&lt;/strong&gt; What can the role do after it is assumed?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A restrictive trust policy does not compensate for an overly broad permissions policy, and vice versa.&lt;/p&gt;
&lt;h2&gt;
  
  
  3. How the OIDC flow works
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fprod-assets.cosmic.aws.dev%2Fa%2F3LSmogCRtnwY5kOGr2xjoS4tzSo%2F02-o.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fprod-assets.cosmic.aws.dev%2Fa%2F3LSmogCRtnwY5kOGr2xjoS4tzSo%2F02-o.webp" title="OIDC Trust Flow" alt="GitHub Actions OIDC trust flow" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 2. AWS validates the token issuer and audience, evaluates the role trust conditions, and STS issues a temporary session.&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A GitHub Actions job starts.&lt;/li&gt;
&lt;li&gt;The workflow is allowed to request an OIDC token by declaring &lt;code&gt;id-token: write&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;GitHub issues a signed token describing the workflow identity.&lt;/li&gt;
&lt;li&gt;The AWS credentials action presents the token to AWS STS using &lt;code&gt;AssumeRoleWithWebIdentity&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;AWS validates the token against the configured GitHub OIDC provider and evaluates the IAM role trust policy.&lt;/li&gt;
&lt;li&gt;If authorized, STS returns temporary credentials.&lt;/li&gt;
&lt;li&gt;The workflow uses those credentials to call permitted AWS APIs. The session expires according to the role session duration.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;id-token: write&lt;/code&gt; permits the job to request an OIDC token; it does not itself grant AWS permissions. AWS authorization still depends on the role trust policy and attached permissions.&lt;/p&gt;
&lt;h2&gt;
  
  
  4. Configure the GitHub OIDC provider in AWS
&lt;/h2&gt;

&lt;p&gt;An IAM OIDC identity provider must exist in the AWS account before a role can trust it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Provider URL:&lt;/strong&gt; &lt;code&gt;https://token.actions.githubusercontent.com&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client ID (audience):&lt;/strong&gt; &lt;code&gt;sts.amazonaws.com&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Create the provider in IAM using the AWS Console or your approved infrastructure-as-code process. Confirm current AWS and GitHub documentation for provider setup and certificate/thumbprint handling rather than copying an old thumbprint from an example.&lt;/p&gt;

&lt;p&gt;The Terraform example in this project accepts the ARN of an already-created GitHub OIDC provider as an input. This keeps provider lifecycle management separate from the example role.&lt;/p&gt;

&lt;p&gt;Do not create duplicate providers for the same issuer in one account; reuse an existing provider where appropriate.&lt;/p&gt;
&lt;h2&gt;
  
  
  5. Understand GitHub's OIDC subject (&lt;code&gt;sub&lt;/code&gt;) claim
&lt;/h2&gt;

&lt;p&gt;One of the most important parts of configuring GitHub Actions OIDC is understanding the &lt;code&gt;sub&lt;/code&gt; (subject) claim.&lt;/p&gt;

&lt;p&gt;AWS uses this claim to determine which GitHub repository and workflow context are permitted to assume an IAM role.&lt;/p&gt;

&lt;p&gt;Historically, the default subject format for a workflow running from the &lt;code&gt;main&lt;/code&gt; branch was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;repo:OWNER/REPOSITORY:ref:refs/heads/main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, GitHub introduced &lt;strong&gt;immutable OIDC subject claims&lt;/strong&gt; to strengthen the security of federated identity.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.1 Legacy subject format
&lt;/h3&gt;

&lt;p&gt;Repositories created before July 15, 2026 generally use the original name-based subject format unless they explicitly opt in to immutable subject claims.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;repo:octo-org/octo-repo:ref:refs/heads/main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This format identifies the repository using its owner and repository names.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.2 New immutable subject format
&lt;/h3&gt;

&lt;p&gt;Repositories created after July 15, 2026 automatically use an immutable subject format by default.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;repo:octo-org@123456/octo-repo@456789:ref:refs/heads/main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The format is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;repo:OWNER@OWNER-ID/REPOSITORY@REPOSITORY-ID:ref:refs/heads/BRANCH
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The new format includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The repository owner's name and immutable ID.&lt;/li&gt;
&lt;li&gt;The repository name and immutable ID.&lt;/li&gt;
&lt;li&gt;The branch context.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The purpose is to prevent identity ambiguity if a repository or organization is renamed, transferred, deleted, or recreated under a previously used name.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.3 Which repositories use which format?
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Repository situation&lt;/th&gt;
&lt;th&gt;Default subject format&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Created before July 15, 2026&lt;/td&gt;
&lt;td&gt;Legacy format, unless opted in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Created after July 15, 2026&lt;/td&gt;
&lt;td&gt;Immutable format&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Renamed after July 15, 2026&lt;/td&gt;
&lt;td&gt;Immutable format&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transferred after July 15, 2026&lt;/td&gt;
&lt;td&gt;Immutable format&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Existing repository explicitly opted in&lt;/td&gt;
&lt;td&gt;Immutable format&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This distinction is important because a trust policy that expects the legacy format will not match a token containing the new immutable format.&lt;/p&gt;

&lt;p&gt;For example, the following two values are different:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;repo:octo-org/octo-repo:ref:refs/heads/main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;repo:octo-org@123456/octo-repo@456789:ref:refs/heads/main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;AWS IAM evaluates an exact &lt;code&gt;StringEquals&lt;/code&gt; condition against the actual claim. If the values do not match, role assumption fails.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; Do not assume which format your repository uses based only on its name or your workflow YAML. Verify the actual token subject before configuring the trust policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Verify the actual OIDC subject claim
&lt;/h2&gt;

&lt;p&gt;Before creating the IAM trust policy, inspect the &lt;code&gt;sub&lt;/code&gt; claim issued by your GitHub Actions workflow.&lt;/p&gt;

&lt;p&gt;This is particularly important for repositories created after July 15, 2026, or repositories that have opted in to immutable subject claims.&lt;/p&gt;

&lt;p&gt;Add a temporary diagnostic step to a GitHub Actions workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Inspect GitHub OIDC subject&lt;/span&gt;
  &lt;span class="na"&gt;shell&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bash&lt;/span&gt;
  &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;ACTIONS_ID_TOKEN_REQUEST_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ env.ACTIONS_ID_TOKEN_REQUEST_URL }}&lt;/span&gt;
    &lt;span class="na"&gt;ACTIONS_ID_TOKEN_REQUEST_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ env.ACTIONS_ID_TOKEN_REQUEST_TOKEN }}&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;set -euo pipefail&lt;/span&gt;

    &lt;span class="s"&gt;TOKEN_RESPONSE=$(curl -sS \&lt;/span&gt;
      &lt;span class="s"&gt;-H "Authorization: Bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \&lt;/span&gt;
      &lt;span class="s"&gt;"${ACTIONS_ID_TOKEN_REQUEST_URL}&amp;amp;audience=sts.amazonaws.com")&lt;/span&gt;

    &lt;span class="s"&gt;TOKEN=$(echo "$TOKEN_RESPONSE" | jq -r '.value')&lt;/span&gt;

    &lt;span class="s"&gt;if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then&lt;/span&gt;
      &lt;span class="s"&gt;echo "Unable to retrieve OIDC token"&lt;/span&gt;
      &lt;span class="s"&gt;exit 1&lt;/span&gt;
    &lt;span class="s"&gt;fi&lt;/span&gt;

    &lt;span class="s"&gt;python3 -c '&lt;/span&gt;
    &lt;span class="s"&gt;import base64&lt;/span&gt;
    &lt;span class="s"&gt;import json&lt;/span&gt;
    &lt;span class="s"&gt;import sys&lt;/span&gt;

    &lt;span class="s"&gt;token = sys.argv[1]&lt;/span&gt;
    &lt;span class="s"&gt;payload = token.split(".")[1]&lt;/span&gt;

    &lt;span class="s"&gt;payload += "=" * (-len(payload) % 4)&lt;/span&gt;

    &lt;span class="s"&gt;claims = json.loads(&lt;/span&gt;
        &lt;span class="s"&gt;base64.urlsafe_b64decode(payload)&lt;/span&gt;
    &lt;span class="s"&gt;)&lt;/span&gt;

    &lt;span class="s"&gt;print("OIDC subject:", claims.get("sub"))&lt;/span&gt;
    &lt;span class="s"&gt;print("OIDC audience:", claims.get("aud"))&lt;/span&gt;
    &lt;span class="s"&gt;' "$TOKEN"&lt;/span&gt;

    &lt;span class="s"&gt;unset TOKEN TOKEN_RESPONSE&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This diagnostic step:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Requests a GitHub OIDC token with the AWS STS audience.&lt;/li&gt;
&lt;li&gt;Decodes the JWT payload locally.&lt;/li&gt;
&lt;li&gt;Displays only the &lt;code&gt;sub&lt;/code&gt; and &lt;code&gt;aud&lt;/code&gt; claims.&lt;/li&gt;
&lt;li&gt;Does not print the complete token.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The token is a temporary credential. Never publish it, save it in build artifacts, or print it in workflow logs.&lt;/p&gt;

&lt;p&gt;After identifying the subject, use the exact value when configuring the AWS IAM trust policy. Remove the diagnostic step when it is no longer required.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Restrict the role trust policy
&lt;/h2&gt;

&lt;p&gt;For a workflow running from the &lt;code&gt;main&lt;/code&gt; branch, the trust policy must match the subject format actually issued by GitHub.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example A: Legacy subject format
&lt;/h3&gt;

&lt;p&gt;For a repository using the legacy format:&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;"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;"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;"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;"Federated"&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::123456789012:oidc-provider/token.actions.githubusercontent.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;"sts:AssumeRoleWithWebIdentity"&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;"token.actions.githubusercontent.com:aud"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sts.amazonaws.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"token.actions.githubusercontent.com:sub"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"repo:octo-org/octo-repo:ref:refs/heads/main"&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;h3&gt;
  
  
  Example B: Immutable subject format
&lt;/h3&gt;

&lt;p&gt;For a repository using the new immutable format:&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;"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;"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;"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;"Federated"&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::123456789012:oidc-provider/token.actions.githubusercontent.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;"sts:AssumeRoleWithWebIdentity"&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;"token.actions.githubusercontent.com:aud"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sts.amazonaws.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"token.actions.githubusercontent.com:sub"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"repo:octo-org@123456/octo-repo@456789:ref:refs/heads/main"&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;p&gt;The account ID, owner name, owner ID, repository name, and repository ID above are illustrative values. Replace them with your actual values.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use only the trust policy that matches your repository's actual subject claim.&lt;/strong&gt; These are alternatives, not two policies that should automatically be combined.&lt;/p&gt;

&lt;h3&gt;
  
  
  Terraform implementation
&lt;/h3&gt;

&lt;p&gt;If you manage the IAM role using Terraform, make the complete expected subject an input rather than assuming every repository uses the old format.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"github_oidc_subject"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Exact GitHub OIDC sub claim for the trusted workflow"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;

  &lt;span class="nx"&gt;validation&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;condition&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trimspace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;github_oidc_subject&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="nx"&gt;error_message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"The GitHub OIDC subject must not be empty."&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;p&gt;Use that input in the role trust policy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_policy_document"&lt;/span&gt; &lt;span class="s2"&gt;"github_actions_trust"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;effect&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
    &lt;span class="nx"&gt;actions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"sts:AssumeRoleWithWebIdentity"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="nx"&gt;principals&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="s2"&gt;"Federated"&lt;/span&gt;
      &lt;span class="nx"&gt;identifiers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;github_oidc_provider_arn&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;condition&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;test&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"StringEquals"&lt;/span&gt;
      &lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"token.actions.githubusercontent.com:aud"&lt;/span&gt;
      &lt;span class="nx"&gt;values&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"sts.amazonaws.com"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;condition&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;test&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"StringEquals"&lt;/span&gt;
      &lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"token.actions.githubusercontent.com:sub"&lt;/span&gt;
      &lt;span class="nx"&gt;values&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;github_oidc_subject&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example input for a legacy repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;github_oidc_subject&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"repo:octo-org/octo-repo:ref:refs/heads/main"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example input for a repository using immutable claims:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;github_oidc_subject&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"repo:octo-org@123456/octo-repo@456789:ref:refs/heads/main"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach makes the trust relationship explicit and avoids silently assuming an outdated subject format.&lt;/p&gt;

&lt;h3&gt;
  
  
  Branches and GitHub Environments
&lt;/h3&gt;

&lt;p&gt;The subject format also depends on the workflow context.&lt;/p&gt;

&lt;p&gt;For example, a workflow using a GitHub Environment may have a subject similar to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;repo:octo-org/octo-repo:environment:production
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For repositories using immutable subjects, the owner and repository IDs are also included.&lt;/p&gt;

&lt;p&gt;When using environments, inspect the actual &lt;code&gt;sub&lt;/code&gt; claim and update the trust policy accordingly. Configure environment protection rules, such as required reviewers and deployment restrictions.&lt;/p&gt;

&lt;p&gt;Do not copy a branch-based trust policy unchanged into an environment-based workflow.&lt;/p&gt;

&lt;p&gt;Avoid broad conditions such as &lt;code&gt;repo:OWNER/*&lt;/code&gt; unless every repository in that scope is intentionally trusted. A wildcard can allow an unintended repository to assume the role.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Grant only the permissions the workflow needs
&lt;/h2&gt;

&lt;p&gt;The example role has a deliberately small read-only S3 policy for one bucket and the &lt;code&gt;reports/&lt;/code&gt; prefix:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;s3:ListBucket&lt;/code&gt; on the specific bucket, constrained by the &lt;code&gt;s3:prefix&lt;/code&gt; condition.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;s3:GetObject&lt;/code&gt; on objects under &lt;code&gt;reports/&lt;/code&gt; in that bucket.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It does not grant bucket creation, object writes, deletion, IAM administration, or access to every S3 bucket.&lt;/p&gt;

&lt;p&gt;For a real pipeline, start with the exact API operations required. Scope resource ARNs wherever the service supports resource-level permissions.&lt;/p&gt;

&lt;p&gt;Some actions require &lt;code&gt;"Resource": "*"&lt;/code&gt;; document why and consider applicable condition keys.&lt;/p&gt;

&lt;p&gt;Do not attach &lt;code&gt;AdministratorAccess&lt;/code&gt; just to make a workflow succeed.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Configure the GitHub Actions workflow
&lt;/h2&gt;

&lt;p&gt;The sample workflow is in &lt;code&gt;.github/workflows/oidc-demo.yml&lt;/code&gt;. It runs on pushes to &lt;code&gt;main&lt;/code&gt; and can also be started manually.&lt;/p&gt;

&lt;p&gt;Its important permissions are:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
  &lt;span class="na"&gt;id-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The workflow configures AWS credentials using a role ARN and Region, then runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws sts get-caller-identity
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This confirms which AWS identity the job is using. It then lists the approved S3 prefix as a permissions demonstration.&lt;/p&gt;

&lt;p&gt;Store the role ARN and bucket name as GitHub Actions repository variables. The role ARN is an identifier, not a secret. The workflow must not contain an AWS access key ID or secret access key.&lt;/p&gt;

&lt;p&gt;For production, pin third-party GitHub Actions to a reviewed full commit SHA rather than relying only on a mutable major-version tag. Review action updates through your normal dependency-management process.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Security considerations
&lt;/h2&gt;

&lt;p&gt;OIDC reduces the need for long-lived credentials, but it does not make a pipeline automatically secure.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Restrict the subject to the required repository and branch or protected environment.&lt;/li&gt;
&lt;li&gt;Protect the trusted branch with reviews and required status checks.&lt;/li&gt;
&lt;li&gt;Review workflow changes: someone who can modify a trusted workflow may change what the role does.&lt;/li&gt;
&lt;li&gt;Use least privilege for AWS actions and resources.&lt;/li&gt;
&lt;li&gt;Grant &lt;code&gt;id-token: write&lt;/code&gt; only to jobs that need federation.&lt;/li&gt;
&lt;li&gt;Protect sensitive environments with required reviewers and deployment rules.&lt;/li&gt;
&lt;li&gt;Pin third-party actions to reviewed immutable references.&lt;/li&gt;
&lt;li&gt;Never print tokens, credentials, or sensitive deployment output.&lt;/li&gt;
&lt;li&gt;Use CloudTrail to review role-assumption and API activity.&lt;/li&gt;
&lt;li&gt;Choose a session duration that is sufficient but not unnecessarily long.&lt;/li&gt;
&lt;li&gt;Verify the repository's actual OIDC subject format before configuring the trust policy.&lt;/li&gt;
&lt;li&gt;Revisit the trust policy if the repository is renamed, transferred, or changes its OIDC subject configuration.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A compromised workflow running in a trusted repository and branch may still obtain the role. OIDC establishes workload identity; repository governance and workflow security remain essential.&lt;/p&gt;

&lt;h2&gt;
  
  
  11. Troubleshooting
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Possible cause&lt;/th&gt;
&lt;th&gt;What to check&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Not authorized to perform sts:AssumeRoleWithWebIdentity&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Trust policy does not match token claims&lt;/td&gt;
&lt;td&gt;Provider ARN, audience, exact &lt;code&gt;sub&lt;/code&gt; condition.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;No OpenIDConnect provider found&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Provider missing or ARN/issuer incorrect&lt;/td&gt;
&lt;td&gt;Confirm provider exists in the same AWS account.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Audience mismatch&lt;/td&gt;
&lt;td&gt;Trust policy expects another audience&lt;/td&gt;
&lt;td&gt;Confirm &lt;code&gt;sts.amazonaws.com&lt;/code&gt; is configured.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Subject mismatch&lt;/td&gt;
&lt;td&gt;Legacy and immutable subject formats differ&lt;/td&gt;
&lt;td&gt;Inspect the actual token's &lt;code&gt;sub&lt;/code&gt; claim and update the trust policy.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Branch mismatch&lt;/td&gt;
&lt;td&gt;Workflow runs from a different branch&lt;/td&gt;
&lt;td&gt;Verify the actual branch context.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Environment mismatch&lt;/td&gt;
&lt;td&gt;Workflow uses a GitHub Environment&lt;/td&gt;
&lt;td&gt;Check the environment-based subject and trust condition.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S3 &lt;code&gt;AccessDenied&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Role assumed, but permissions are insufficient&lt;/td&gt;
&lt;td&gt;Check attached policy, bucket policy, prefix, and organization controls.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Workflow cannot request OIDC token&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;id-token: write&lt;/code&gt; missing or overridden&lt;/td&gt;
&lt;td&gt;Check workflow/job permissions.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws sts get-caller-identity
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;after configuring credentials.&lt;/p&gt;

&lt;p&gt;If it succeeds, role assumption worked. An access-denied error on a later S3 command is a separate permissions issue.&lt;/p&gt;

&lt;h2&gt;
  
  
  12. Cost considerations
&lt;/h2&gt;

&lt;p&gt;OIDC federation does not require a continuously running compute service. However, AWS API usage and the resources your workflow operates can incur charges. GitHub Actions usage is subject to GitHub's plan and runner limits.&lt;/p&gt;

&lt;p&gt;Check current pricing and usage policies before scaling automation.&lt;/p&gt;

&lt;h2&gt;
  
  
  13. Validation checklist
&lt;/h2&gt;

&lt;p&gt;This project has been deployed or tested. In a dedicated non-production account:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Validation&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GitHub OIDC authentication&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IAM role assumption&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Approved S3 prefix access&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unauthorized prefix access&lt;/td&gt;
&lt;td&gt;PASS — AccessDenied&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CloudTrail verification&lt;/td&gt;
&lt;td&gt;COMPLETED&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resource cleanup&lt;/td&gt;
&lt;td&gt;COMPLETED&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  14. When should you use this pattern?
&lt;/h2&gt;

&lt;p&gt;Use OIDC federation when an external workload such as GitHub Actions needs AWS access and supports issuing a trusted identity token.&lt;/p&gt;

&lt;p&gt;Examples include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CI pipelines that publish artifacts.&lt;/li&gt;
&lt;li&gt;Application deployment workflows.&lt;/li&gt;
&lt;li&gt;Infrastructure automation running Terraform against AWS.&lt;/li&gt;
&lt;li&gt;Automated testing that needs temporary access to AWS resources.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The trust conditions and permissions should differ by workload. A build job that uploads an artifact should not automatically receive the same role as a production deployment job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Long-lived access keys are not the only way for automation to authenticate to AWS. With OIDC federation, GitHub Actions can present a signed workload identity to AWS STS and receive temporary credentials for a narrowly scoped IAM role.&lt;/p&gt;

&lt;p&gt;The strongest design combines a precise trust policy, least-privilege permissions, protected source branches, reviewed workflow changes, and CloudTrail visibility.&lt;/p&gt;

&lt;p&gt;GitHub's introduction of immutable OIDC subject claims makes it especially important to verify the actual subject before configuring AWS IAM. A trust policy must match the identity GitHub really issues, not simply a format copied from an older example.&lt;/p&gt;

&lt;p&gt;OIDC reduces credential-management risk, but it does not replace secure CI/CD practices.&lt;/p&gt;

&lt;p&gt;The central lesson is to treat the pipeline as an identity with a defined trust boundary: decide exactly which workload may assume the role, what it may do, and how its activity will be reviewed.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_create_oidc.html" rel="noopener noreferrer"&gt;AWS IAM: Creating an OIDC identity provider&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-idp_oidc.html" rel="noopener noreferrer"&gt;AWS IAM: Creating a role for OIDC federation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html" rel="noopener noreferrer"&gt;AWS STS AssumeRoleWithWebIdentity&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.github.com/en/actions/how-tos/secure-your-work/security-harden-deployments/oidc-in-aws" rel="noopener noreferrer"&gt;GitHub Actions: Configuring OIDC in AWS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.github.com/en/actions/reference/security/oidc" rel="noopener noreferrer"&gt;GitHub Actions: OpenID Connect reference — including immutable subject claims&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.blog/changelog/2026-04-23-immutable-subject-claims-for-github-actions-oidc-tokens/" rel="noopener noreferrer"&gt;GitHub Changelog: Immutable subject claims for GitHub Actions OIDC tokens&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions" rel="noopener noreferrer"&gt;GitHub Actions workflow permissions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html" rel="noopener noreferrer"&gt;AWS IAM best practices&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>devops</category>
      <category>security</category>
      <category>githubactions</category>
    </item>
    <item>
      <title>Building a Serverless Event-Driven Processing Pipeline with Amazon S3, Amazon SQS, and AWS Lambda</title>
      <dc:creator>Narendra | SRE</dc:creator>
      <pubDate>Sat, 31 Oct 2026 14:38:47 +0000</pubDate>
      <link>https://dev.to/narendrasre/building-a-serverless-event-driven-processing-pipeline-with-amazon-s3-amazon-sqs-and-aws-lambda-2nob</link>
      <guid>https://dev.to/narendrasre/building-a-serverless-event-driven-processing-pipeline-with-amazon-s3-amazon-sqs-and-aws-lambda-2nob</guid>
      <description>&lt;h1&gt;
  
  
  Building a Serverless Event-Driven Processing Pipeline with Amazon S3, Amazon SQS, and AWS Lambda
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Level:&lt;/strong&gt; Intermediate&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Focus:&lt;/strong&gt; Event-driven architecture, asynchronous processing, reliability, security, and observability&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Example code:&lt;/strong&gt; &lt;a href="https://github.com/narendra-sre/aws-community-builders/tree/main/article-01-serverless-event-pipeline" rel="noopener noreferrer"&gt;aws-community-builders repository&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Implementation status:&lt;/strong&gt; This reference implementation has been deployed and validated in an AWS learning environment in the Asia Pacific (Hyderabad) Region (&amp;nbsp;ap-south-2&amp;nbsp;).&lt;/p&gt;

&lt;p&gt;The implementation was provisioned using Terraform and tested with real S3 uploads, SQS message delivery, Lambda processing, failure handling, retries, dead-letter queue routing, and&lt;br&gt;
CloudWatch monitoring. Validation evidence is available in the accompanying GitHub repository.&lt;/p&gt;

&lt;p&gt;This is a learning reference implementation, not a production-ready solution. Review the security, reliability, and cost considerations before adapting it for production.&lt;/p&gt;

&lt;p&gt;Originally published on &lt;a href="https://builder.aws.com/content/3LHlpNFoF3M5IgwTWdfkn4uKm4L/building-a-serverless-event-driven-processing-pipeline-with-amazon-s3-amazon-sqs-and-aws-lambda" rel="noopener noreferrer"&gt;AWS Community Builder&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;p&gt;Imagine you operate an application where users upload invoices, images, reports, or CSV files. Each upload needs to trigger some work: validate metadata, extract information, transform data, or notify another system.&lt;/p&gt;

&lt;p&gt;A tightly coupled implementation can make the upload path slower and harder to operate. If processing is temporarily unavailable, the upload workflow may also be affected. A more resilient approach is to separate the act of accepting a file from the work that follows it.&lt;/p&gt;

&lt;p&gt;In this article, we'll explore an event-driven pattern built with Amazon Simple Storage Service (Amazon S3), Amazon Simple Queue Service (Amazon SQS), and AWS Lambda. We'll also cover a dead-letter queue, IAM permissions, encryption, monitoring, retries, and idempotency.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F137gg81h578v01dumnzm.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F137gg81h578v01dumnzm.png" alt=" " width="800" height="137"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 1. A file-created event is queued, processed asynchronously by Lambda, and observed through CloudWatch. Repeatedly failing messages can be moved to a dead-letter queue.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The problem we're solving
&lt;/h2&gt;

&lt;p&gt;A simple file upload workflow often starts with one question: what should happen after a file arrives?&lt;/p&gt;

&lt;p&gt;For a small prototype, an application might upload a file and then call a processing function directly. This can be acceptable, but it creates a tighter dependency between the upload path and the processing operation. Slow processing, transient errors, or sudden bursts of uploads can complicate the user experience and recovery process.&lt;/p&gt;

&lt;p&gt;The design goal is to make file storage and file processing separate responsibilities. We want to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Accept files without requiring the processing job to finish immediately.&lt;/li&gt;
&lt;li&gt;Buffer events when uploads arrive faster than they can be processed.&lt;/li&gt;
&lt;li&gt;Retry temporary failures.&lt;/li&gt;
&lt;li&gt;Isolate repeatedly failing messages for investigation.&lt;/li&gt;
&lt;li&gt;Limit permissions to what each component needs.&lt;/li&gt;
&lt;li&gt;Make activity and failures visible to operators.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2. Architecture overview
&lt;/h2&gt;

&lt;p&gt;The main components are:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Amazon S3&lt;/td&gt;
&lt;td&gt;Stores uploaded objects and emits object-created notifications.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Amazon SQS processing queue&lt;/td&gt;
&lt;td&gt;Buffers notifications so processing can happen asynchronously.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS Lambda&lt;/td&gt;
&lt;td&gt;Polls the queue through an event source mapping and handles messages in batches.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Amazon SQS dead-letter queue (DLQ)&lt;/td&gt;
&lt;td&gt;Holds messages that could not be processed after the configured retry limit.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS CloudWatch Logs&lt;/td&gt;
&lt;td&gt;Stores Lambda execution logs for troubleshooting.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS IAM&lt;/td&gt;
&lt;td&gt;Defines which actions the Lambda execution role can perform.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The flow is deliberately asynchronous. S3 publishes an event notification to SQS; Lambda polls SQS using an event source mapping. This differs from the model where S3 invokes Lambda directly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why put SQS between S3 and Lambda?
&lt;/h3&gt;

&lt;p&gt;SQS acts as a buffer. If a burst of uploads creates more work than Lambda can process immediately, messages can wait in the queue. The queue also gives the consumer a retry mechanism and a place to route messages that repeatedly fail.&lt;/p&gt;

&lt;p&gt;A queue does not make processing exactly-once. Standard SQS queues provide at-least-once delivery, so a message may be delivered more than once. The consumer should be designed to tolerate duplicates.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. End-to-end event flow
&lt;/h2&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvbati4eqslekazcmbcik.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvbati4eqslekazcmbcik.png" alt=" " width="800" height="910"&gt;&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;em&gt;Figure 2. Upload, event notification, queueing, Lambda polling, processing, and logging in sequence.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Let's walk through the request lifecycle:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A user uploads an object to S3.&lt;/strong&gt; The bucket stores the object. The bucket can be configured to send object-created notifications to a designated SQS queue.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;S3 publishes a notification.&lt;/strong&gt; The queue policy must allow the expected S3 bucket to send messages. Restrict the policy to the intended source bucket and account where applicable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQS retains the message.&lt;/strong&gt; The message stays in the queue until the consumer handles it successfully or the redrive policy sends it to the DLQ.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lambda polls the queue.&lt;/strong&gt; The Lambda event source mapping retrieves batches of messages and invokes the function.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The function validates and processes each record.&lt;/strong&gt; In a real implementation, processing might read object metadata, parse a file, or submit a downstream task. The example handler logs object details as a small demonstration rather than performing expensive file transformations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Successful records are acknowledged.&lt;/strong&gt; Lambda/SQS integration deletes successfully processed messages after invocation handling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failures are retried.&lt;/strong&gt; With partial batch response support enabled, the function can report individual failed message identifiers so successful records in the same batch do not have to be retried unnecessarily.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logs support investigation.&lt;/strong&gt; Operators can review Lambda logs and queue metrics to understand processing volume and failures.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  A practical note about payloads
&lt;/h3&gt;

&lt;p&gt;S3 event notifications normally contain bucket and object-key details. Object keys in event records are URL-encoded; decode them before using them as literal keys. If the application needs to read the object body, the Lambda execution role also needs narrowly scoped S3 read access. Don't grant object access merely because the event contains a bucket and key.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Handling failures: retries and a dead-letter queue
&lt;/h2&gt;

&lt;p&gt;Failures are part of normal distributed-system operation. A downstream dependency may time out, a file may be malformed, or the function may encounter a bug.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1oez5iax4q7z7besg7e2.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1oez5iax4q7z7besg7e2.png" alt=" " width="800" height="673"&gt;&lt;/a&gt; &lt;br&gt;
&lt;em&gt;Figure 3. Failed messages can be retried by SQS/Lambda integration and moved to a dead-letter queue after the configured receive limit.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A dead-letter queue helps separate messages that need attention from the main processing backlog. The queue's redrive policy defines the maximum receive count before a message is moved to the DLQ.&lt;/p&gt;

&lt;p&gt;When a message lands in the DLQ:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Inspect the body and message attributes to understand the failure.&lt;/li&gt;
&lt;li&gt;Correlate the message with Lambda logs using timestamps and application identifiers.&lt;/li&gt;
&lt;li&gt;Determine whether the issue was transient, data-specific, or caused by a code defect.&lt;/li&gt;
&lt;li&gt;Fix the underlying problem before redriving messages to the source queue.&lt;/li&gt;
&lt;li&gt;Avoid blindly replaying a large DLQ into an unhealthy downstream service.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Partial batch failures
&lt;/h3&gt;

&lt;p&gt;Lambda can receive multiple SQS messages in one invocation. Without partial batch response handling, one failed record can cause the whole batch to be retried. When &lt;code&gt;ReportBatchItemFailures&lt;/code&gt; is enabled, the function returns the message identifiers for records that failed, allowing successfully processed records to be acknowledged separately.&lt;/p&gt;

&lt;p&gt;The handler must return the correct SQS message identifiers for failures. This improves retry efficiency but does not remove the need for idempotency.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Idempotency: preparing for duplicate delivery
&lt;/h2&gt;

&lt;p&gt;For standard queues, at-least-once delivery means duplicate processing is possible. For example, a function might complete a side effect but fail before the message is acknowledged. The same message can then be delivered again.&lt;/p&gt;

&lt;p&gt;An idempotent operation produces the same effective result when repeated with the same input. Useful techniques include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use a stable business identifier or a combination of bucket, object key, object version, and event metadata to identify the work item.&lt;/li&gt;
&lt;li&gt;Store processing status in a durable data store when the business operation requires deduplication.&lt;/li&gt;
&lt;li&gt;Use conditional writes or unique constraints to prevent duplicate side effects.&lt;/li&gt;
&lt;li&gt;Make downstream APIs idempotent when they support idempotency keys.&lt;/li&gt;
&lt;li&gt;Consider S3 versioning if the processing semantics depend on a particular object version.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The simple example in the repository demonstrates event handling and partial batch failure structure. It is not a complete business-level deduplication system.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Security design: least privilege and protected storage
&lt;/h2&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv3ec1rqyv4da65rj2h1p.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv3ec1rqyv4da65rj2h1p.png" alt=" " width="800" height="307"&gt;&lt;/a&gt; &lt;br&gt;
&lt;em&gt;Figure 4. Each service has a specific permission boundary: S3 may publish to the queue, Lambda may poll the queue and write logs, and bucket access remains private by default.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Security should be designed into the workflow rather than added after it works.&lt;/p&gt;

&lt;h3&gt;
  
  
  S3 bucket controls
&lt;/h3&gt;

&lt;p&gt;The Terraform example configures S3 Block Public Access, server-side encryption, and versioning. These are useful baseline controls, but they do not replace a complete data-protection policy. For production, consider retention requirements, lifecycle rules, access logging where appropriate, and whether a customer-managed KMS key is required.&lt;/p&gt;

&lt;h3&gt;
  
  
  Queue policy
&lt;/h3&gt;

&lt;p&gt;The queue policy should permit &lt;code&gt;s3.amazonaws.com&lt;/code&gt; to send messages only from the intended bucket and account context. Avoid a broad policy that allows arbitrary sources to publish messages to the queue.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lambda execution role
&lt;/h3&gt;

&lt;p&gt;The Lambda role should contain only the permissions required for the function to operate: write its CloudWatch logs, receive/delete messages, inspect queue attributes, and access other resources only when the application requires them. The example should be reviewed carefully before production use, including the exact actions and resource ARNs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Encryption
&lt;/h3&gt;

&lt;p&gt;S3 server-side encryption protects stored objects. SQS encryption can be configured separately; if using a customer-managed KMS key, ensure the relevant principals have the necessary key permissions. Encryption configuration does not, by itself, determine who is authorized to read a resource.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keep credentials out of code
&lt;/h3&gt;

&lt;p&gt;Use an appropriate AWS credential provider or role-based access rather than embedding access keys in source files. Never commit real credentials, account identifiers that you intend to keep private, customer data, or secrets into the repository.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Observability and operational signals
&lt;/h2&gt;

&lt;p&gt;CloudWatch Logs are the starting point for inspecting function execution, but logs alone are not enough to operate a queue-based workflow.&lt;/p&gt;

&lt;p&gt;Monitor signals such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ApproximateNumberOfMessagesVisible:&lt;/strong&gt; messages waiting to be processed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ApproximateAgeOfOldestMessage:&lt;/strong&gt; how long the oldest queued message has been waiting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NumberOfMessagesSent / NumberOfMessagesReceived:&lt;/strong&gt; useful for understanding queue activity, with the usual caveat that these are approximate queue metrics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lambda errors and throttles:&lt;/strong&gt; indicators that processing is failing or capacity is constrained.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DLQ visible message count:&lt;/strong&gt; a useful alert signal for messages that need investigation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a production deployment, define alarms using the service metrics that matter to the workload, set retention for logs, and document the response procedure when alerts fire. Avoid logging full file contents or sensitive values.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Cost considerations
&lt;/h2&gt;

&lt;p&gt;The architecture avoids managing long-running servers, but serverless does not mean free. Charges may arise from S3 storage and requests, SQS requests, Lambda invocations and duration, CloudWatch log ingestion and storage, data transfer, and optional encryption or monitoring features.&lt;/p&gt;

&lt;p&gt;Before running the example:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check the current AWS Free Tier terms and eligibility for your account. Offerings and eligibility rules can change.&lt;/li&gt;
&lt;li&gt;Use a dedicated test bucket and a small number of test objects.&lt;/li&gt;
&lt;li&gt;Configure log retention rather than keeping logs indefinitely by default.&lt;/li&gt;
&lt;li&gt;Delete test resources after validating the workflow.&lt;/li&gt;
&lt;li&gt;Review AWS Billing and Cost Management and consider a budget alert. A budget alert is not a hard spending cap and may arrive after charges have accrued.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This article intentionally does not promise a zero-cost deployment. Actual costs depend on your account eligibility, Region, request volume, payload sizes, logging, and usage duration.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Implementation and Validation
&lt;/h2&gt;

&lt;p&gt;The companion repository contains the Terraform implementation, Python Lambda handler, and supporting validation evidence.&lt;/p&gt;

&lt;p&gt;The infrastructure was deployed in AWS using Terraform.&lt;br&gt;
The deployment created 14 resources successfully.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deployment validation
&lt;/h3&gt;

&lt;p&gt;The following activities were performed:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Provisioned the infrastructure using Terraform.&lt;/li&gt;
&lt;li&gt;Uploaded a test object to the S3 bucket.&lt;/li&gt;
&lt;li&gt;Verified S3 event notification delivery through SQS.&lt;/li&gt;
&lt;li&gt;Confirmed successful Lambda execution.&lt;/li&gt;
&lt;li&gt;Inspected Lambda execution logs in CloudWatch.&lt;/li&gt;
&lt;li&gt;Tested intentional processing failures.&lt;/li&gt;
&lt;li&gt;Verified SQS retry behaviour.&lt;/li&gt;
&lt;li&gt;Verified failed-message delivery to the DLQ.&lt;/li&gt;
&lt;li&gt;Inspected CloudWatch DLQ alarm configuration and state.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Evidence
&lt;/h3&gt;

&lt;p&gt;The repository includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Terraform deployment outputs.&lt;/li&gt;
&lt;li&gt;Terraform infrastructure state.&lt;/li&gt;
&lt;li&gt;Successful Lambda processing logs.&lt;/li&gt;
&lt;li&gt;Failed processing logs.&lt;/li&gt;
&lt;li&gt;SQS processing queue attributes.&lt;/li&gt;
&lt;li&gt;DLQ attributes.&lt;/li&gt;
&lt;li&gt;Lambda event source mapping configuration.&lt;/li&gt;
&lt;li&gt;CloudWatch alarm information.&lt;/li&gt;
&lt;li&gt;AWS Console screenshots.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;See the &amp;nbsp;&lt;code&gt;evidence/&lt;/code&gt;&amp;nbsp; directory for the corresponding outputs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cleanup
&lt;/h3&gt;

&lt;p&gt;After completing validation and collecting the required evidence,&lt;br&gt;
the test infrastructure was removed using Terraform.&lt;/p&gt;

&lt;p&gt;This prevents unnecessary ongoing resource charges.&lt;/p&gt;

&lt;p&gt;The example should be deployed in a controlled AWS account.&lt;br&gt;
Always review the Terraform plan and estimated costs before applying it.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. When would I use this architecture?
&lt;/h2&gt;

&lt;p&gt;This pattern is useful when a storage event should start background work and the system benefits from buffering and retry handling. Examples include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Validating uploaded CSV files.&lt;/li&gt;
&lt;li&gt;Starting image or document processing.&lt;/li&gt;
&lt;li&gt;Running metadata extraction jobs.&lt;/li&gt;
&lt;li&gt;Kicking off asynchronous data-import workflows.&lt;/li&gt;
&lt;li&gt;Separating ingestion from downstream processing in internal tools.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It may be unnecessary for a tiny workflow where a direct S3-to-Lambda integration is sufficient. It also isn't a universal solution for long-running orchestration, strict ordering, or exactly-once business effects. Those requirements need additional design choices.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Combining Amazon S3, Amazon SQS, and AWS Lambda provides a practical way to build asynchronous, event-driven processing systems.&lt;/p&gt;

&lt;p&gt;This architecture separates object storage from processing, provides message buffering, supports retries, and isolates repeatedly failing messages through a dead-letter queue.&lt;/p&gt;

&lt;p&gt;The implementation also demonstrates why security, observability, and operational validation are important parts of cloud architecture.&lt;/p&gt;

&lt;p&gt;By deploying and testing this architecture with Terraform, we can move beyond understanding individual AWS services and learn how they work together in a real cloud environment.&lt;/p&gt;

&lt;p&gt;The complete implementation and validation evidence are available&lt;br&gt;
in the companion GitHub repository.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/EventNotifications.html" rel="noopener noreferrer"&gt;Amazon S3 event notifications&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/with-sqs.html" rel="noopener noreferrer"&gt;Using Lambda with Amazon SQS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/services-sqs-errorhandling.html" rel="noopener noreferrer"&gt;Handling errors for an SQS event source in Lambda&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-dead-letter-queues.html" rel="noopener noreferrer"&gt;Using dead-letter queues in Amazon SQS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/security-best-practices.html" rel="noopener noreferrer"&gt;Security best practices for Amazon S3&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/lambda/pricing/" rel="noopener noreferrer"&gt;AWS Lambda pricing&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Author note before publication:&lt;/strong&gt; Add your own short paragraph about why you chose this pattern and what you learned while reviewing the implementation. Once you've actually tested it, update the implementation-status note with the specific checks you completed and any limitations you observed. Do not claim deployment or test results before doing them.&lt;/p&gt;

</description>
      <category>eventdriven</category>
      <category>serverless</category>
      <category>amazonsqs</category>
      <category>amazonlambda</category>
    </item>
    <item>
      <title>What Really Happens When You Type a URL in Your Browser?</title>
      <dc:creator>Narendra | SRE</dc:creator>
      <pubDate>Sat, 23 May 2026 11:03:23 +0000</pubDate>
      <link>https://dev.to/narendrasre/what-really-happens-when-you-type-a-url-in-your-browser-3cm1</link>
      <guid>https://dev.to/narendrasre/what-really-happens-when-you-type-a-url-in-your-browser-3cm1</guid>
      <description>&lt;p&gt;You type a URL. Press Enter. A beautiful website appears in milliseconds. But behind that single action, an entire chain of networking, security, and system-level operations takes place.&lt;/p&gt;

&lt;p&gt;If you're learning DevOps, SRE, Backend Engineering, Networking, or System Design - this is one of the most important flows to understand.&lt;/p&gt;

&lt;h2&gt;
  
  
  🌍 Example
&lt;/h2&gt;

&lt;p&gt;Suppose you type:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🌍&lt;a href="https://google.com" rel="noopener noreferrer"&gt;https://google.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;what happens internally?&lt;br&gt;
Let's break it down step-by-step.&lt;/p&gt;

&lt;h2&gt;
  
  
  ⚡ High-Level Flow
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Enter URL&lt;br&gt;
↓&lt;br&gt;
Browser Cache Check&lt;br&gt;
↓&lt;br&gt;
DNS Resolution&lt;br&gt;
↓&lt;br&gt;
TCP Handshake&lt;br&gt;
↓&lt;br&gt;
TLS/SSL Handshake&lt;br&gt;
↓&lt;br&gt;
HTTP Request Sent&lt;br&gt;
↓&lt;br&gt;
Load Balancer / Server&lt;br&gt;
↓&lt;br&gt;
Application Processing&lt;br&gt;
↓&lt;br&gt;
HTTP Response&lt;br&gt;
↓&lt;br&gt;
Browser Rendering&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  1. Browser Checks Cache First
&lt;/h2&gt;

&lt;p&gt;Before contacting any server, the browser checks whether it already knows the IP address.&lt;br&gt;
It checks:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;✔ Browser cache&lt;br&gt;
✔ OS DNS cache&lt;br&gt;
✔ Router cache&lt;br&gt;
✔ ISP cache&lt;br&gt;
If found:&lt;/p&gt;

&lt;p&gt;Use cache IP address&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This avoids unnecessary DNS lookups and makes websites load faster.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. DNS Resolution Happens
&lt;/h2&gt;

&lt;p&gt;Humans remember names. Computer communicates using IP addresses.&lt;/p&gt;

&lt;p&gt;So the browser asks:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What is the IP address of google.com?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The DNS server responds with something like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;142.250.xxx.xxx&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This process is called DNS Resolution.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. TCP Connection is Established
&lt;/h2&gt;

&lt;p&gt;Now the browser knows the server IP.&lt;/p&gt;

&lt;p&gt;Next step:&lt;/p&gt;

&lt;h3&gt;
  
  
  🤝 TCP 3-Way Handshake
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Client → SYN&lt;br&gt;
Server → SYN-ACK&lt;br&gt;
Client → ACK&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now a reliable connection is established.&lt;/p&gt;

&lt;h3&gt;
  
  
  📌 Why TCP?
&lt;/h3&gt;

&lt;p&gt;TCP guarantees:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;✔ Reliable delivery&lt;br&gt;
✔ Ordered packets&lt;br&gt;
✔ Error checking&lt;br&gt;
✔ Retransmission&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is extremely important for web applications.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. TLS/SSL Handshake (HTTPS)
&lt;/h2&gt;

&lt;p&gt;Since the URL starts with:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;https://&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A secure encrypted connection must be created.&lt;/p&gt;

&lt;p&gt;During TLS handshake:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;✔ Certificate validation&lt;br&gt;
✔ Encryption negotiation&lt;br&gt;
✔ Session key exchange&lt;br&gt;
✔ Secure communication setup&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now communication becomes encrypted.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Browser Sends HTTP Request
&lt;/h2&gt;

&lt;p&gt;Now the browser sends:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;GET / HTTP/1.1&lt;br&gt;
Host: google.com&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The request also contains:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;✔ Headers&lt;br&gt;
✔ Cookies&lt;br&gt;
✔ Authentication tokens&lt;br&gt;
✔ User-Agent&lt;br&gt;
✔ Compression support&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  6. Request Travels Across the Internet
&lt;/h2&gt;

&lt;p&gt;The request moves through multiple layers:&lt;/p&gt;

&lt;p&gt;Browser&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;→ Operating System&lt;br&gt;
→ Router&lt;br&gt;
→ ISP&lt;br&gt;
→ Internet&lt;br&gt;
→ CDN / Load Balancer&lt;br&gt;
→ Web Server&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In modern production systems:&lt;/p&gt;

&lt;p&gt;Requests usually hit CDN or Load Balancer first&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Server Processes the Request
&lt;/h2&gt;

&lt;p&gt;Now the backend application starts working.&lt;/p&gt;

&lt;p&gt;Possible operations:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;✔ Run application logic&lt;br&gt;
✔ Authenticate user&lt;br&gt;
✔ Query databases&lt;br&gt;
✔ Read cache&lt;br&gt;
✔ Call microservices&lt;br&gt;
✔ Fetch files&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The server then prepares a response.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Server Sends HTTP Response
&lt;/h2&gt;

&lt;p&gt;Example response:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;HTTP/1.1 200 OK&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The server may return:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;✔ HTML&lt;br&gt;
✔ CSS&lt;br&gt;
✔ JavaScript&lt;br&gt;
✔ Images&lt;br&gt;
✔ JSON data&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  9. Browser Renders the Webpage
&lt;/h2&gt;

&lt;p&gt;Now the browser starts rendering.&lt;/p&gt;

&lt;p&gt;Internally it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;✔ Parses HTML&lt;br&gt;
✔ Builds DOM tree&lt;br&gt;
✔ Downloads CSS&lt;br&gt;
✔ Executes JavaScript&lt;br&gt;
✔ Paints pixels on screen&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Finally:&lt;/p&gt;

&lt;h2&gt;
  
  
  🎉 The webpage appears
&lt;/h2&gt;

&lt;h2&gt;
  
  
  🎯 Interview Keywords You Must Know
&lt;/h2&gt;

&lt;p&gt;DNS&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;TCP Handshake&lt;br&gt;
TLS Handshake&lt;br&gt;
HTTP Request/Response&lt;br&gt;
Caching&lt;br&gt;
CDN&lt;br&gt;
Load Balancer&lt;br&gt;
Browser Rendering&lt;br&gt;
Latency&lt;br&gt;
Microservices&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  🔥 If You Found This Useful
&lt;/h2&gt;

&lt;p&gt;Follow for more content on:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;DevOps&lt;br&gt;
SRE&lt;br&gt;
Linux&lt;br&gt;
Kubernetes&lt;br&gt;
Networking&lt;br&gt;
System Design&lt;br&gt;
Production Engineering&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Happy Learning&lt;/p&gt;

</description>
      <category>networking</category>
      <category>cloud</category>
      <category>dns</category>
      <category>linux</category>
    </item>
  </channel>
</rss>
