<?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: Ilyas Rufai</title>
    <description>The latest articles on DEV Community by Ilyas Rufai (@rufilboss).</description>
    <link>https://dev.to/rufilboss</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%2F548489%2F0921b258-f827-4615-a9cf-d5c6f016875b.jpg</url>
      <title>DEV Community: Ilyas Rufai</title>
      <link>https://dev.to/rufilboss</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rufilboss"/>
    <language>en</language>
    <item>
      <title>What Is the Secrets Management Maturity Model for DevSecOps Teams?</title>
      <dc:creator>Ilyas Rufai</dc:creator>
      <pubDate>Mon, 13 Jul 2026 11:45:39 +0000</pubDate>
      <link>https://dev.to/rufilboss/what-is-the-secrets-management-maturity-model-for-devsecops-teams-19l0</link>
      <guid>https://dev.to/rufilboss/what-is-the-secrets-management-maturity-model-for-devsecops-teams-19l0</guid>
      <description>&lt;p&gt;A contractor leaves. Nobody knows which &lt;code&gt;.env&lt;/code&gt; files still hold production database passwords. Continuous integration (CI) uses the same AWS access key in staging and production. A secret scanner fires on Friday night, and the on-call engineer rotates nothing because "we will deal with it Monday."&lt;/p&gt;

&lt;p&gt;That is not a tooling problem alone. It is a &lt;strong&gt;maturity&lt;/strong&gt; problem. Teams buy HashiCorp Vault or AWS Secrets Manager and still behave like secrets live in chat logs and shared spreadsheets.&lt;/p&gt;

&lt;p&gt;In this article, you will learn a five-level &lt;strong&gt;secrets management maturity model&lt;/strong&gt; for DevSecOps teams. You will assess where your organization sits today, recognize the signals at each level, and follow concrete steps to move up without boiling the ocean.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who this is for:&lt;/strong&gt; Platform engineers, DevSecOps engineers, security champions, and engineering leads responsible for credentials in cloud and CI/CD environments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you will learn:&lt;/strong&gt; How to map people, process, and technology practices to maturity levels, and what to fix first at each stage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Basic familiarity with environment variables, API keys, and cloud identity&lt;/li&gt;
&lt;li&gt;Awareness of your team's current secret storage (even if the answer is uncomfortable)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Related work:&lt;/strong&gt; For the detection layer that catches secrets before merge, see &lt;a href="https://dev.to/rufilboss/secret-scanning-in-ci-what-pre-commit-pull-request-and-main-branch-each-actually-catch-4k99"&gt;Secret Scanning in CI: Three Layers with Gitleaks&lt;/a&gt;. For identity instead of keys on AWS compute, see &lt;a href="https://dev.to/rufilboss/terraform-ec2-and-s3-upload-access-a-simple-iam-role-setup-without-access-keys-4k99"&gt;Terraform EC2 and S3 Upload Access&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Level 1: Ad hoc:&lt;/strong&gt; secrets in code, chat, and local files; no owner, no rotation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Level 2: Centralized:&lt;/strong&gt; A vault or cloud secret store exists, but access is manual and broad.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Level 3: Guarded:&lt;/strong&gt; secrets stay out of git, scanners block merges, and rotation has a calendar.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Level 4: Short-lived:&lt;/strong&gt; workloads and CI use roles, OIDC, or dynamic credentials, not standing keys.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Level 5: Governed:&lt;/strong&gt; automated rotation, audit evidence, break-glass, and blast-radius controls are standard.&lt;/li&gt;
&lt;li&gt;You do not skip levels. You stabilize each before adding complexity.&lt;/li&gt;
&lt;li&gt;Measure maturity with observable signals: git history, CloudTrail, scanner results, and time-to-revoke after offboarding.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Why "We Will Use a Vault Later" Fails&lt;/li&gt;
&lt;li&gt;What the Maturity Model Is (and Is Not)&lt;/li&gt;
&lt;li&gt;The Five Levels at a Glance&lt;/li&gt;
&lt;li&gt;Level 1: Ad Hoc Secrets&lt;/li&gt;
&lt;li&gt;Level 2: Centralized Storage&lt;/li&gt;
&lt;li&gt;Level 3: Guarded Delivery&lt;/li&gt;
&lt;li&gt;Level 4: Short-Lived Credentials&lt;/li&gt;
&lt;li&gt;Level 5: Governed Lifecycle&lt;/li&gt;
&lt;li&gt;How to Assess Your Current Level&lt;/li&gt;
&lt;li&gt;How to Move Up One Level at a Time&lt;/li&gt;
&lt;li&gt;How to Map Tools to Maturity Levels&lt;/li&gt;
&lt;li&gt;How to Verify Progress&lt;/li&gt;
&lt;li&gt;When This Breaks Down&lt;/li&gt;
&lt;li&gt;Frequently Asked Questions&lt;/li&gt;
&lt;li&gt;Conclusion&lt;/li&gt;
&lt;li&gt;References&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why "We Will Use a Vault Later" Fails
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Common habit&lt;/th&gt;
&lt;th&gt;What breaks&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Shared &lt;code&gt;.env&lt;/code&gt; files in Slack or email&lt;/td&gt;
&lt;td&gt;No audit trail; offboarding does not revoke access&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One production API key reused in CI, scripts, and laptops&lt;/td&gt;
&lt;td&gt;Single leak compromises every path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vault installed but apps still read &lt;code&gt;process.env&lt;/code&gt; from flat files&lt;/td&gt;
&lt;td&gt;Tooling theater, maturity does not move&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rotation "scheduled" with no owner&lt;/td&gt;
&lt;td&gt;Keys age past policy; audits fail quietly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scanner alerts ignored as false positives&lt;/td&gt;
&lt;td&gt;Real leaks hide in noise&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; Maturity is the combination of &lt;strong&gt;where secrets live&lt;/strong&gt;, &lt;strong&gt;who can access them&lt;/strong&gt;, &lt;strong&gt;how long they last&lt;/strong&gt;, and &lt;strong&gt;what you can prove&lt;/strong&gt; after an incident.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Maturity Model Is (and Is Not)
&lt;/h2&gt;

&lt;p&gt;This model is a &lt;strong&gt;practical assessment framework&lt;/strong&gt; for engineering teams. It is not a compliance certification, and it is not tied to one vendor.&lt;/p&gt;

&lt;p&gt;It answers four questions at each level:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Storage:&lt;/strong&gt; Where do secrets live?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delivery:&lt;/strong&gt; How do apps and pipelines receive them?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lifecycle:&lt;/strong&gt; How are secrets created, rotated, and revoked?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evidence:&lt;/strong&gt; What logs and metrics prove control?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can use it in a one-hour workshop with platform and application leads. Score each domain honestly. Your &lt;strong&gt;lowest&lt;/strong&gt; domain sets your effective level.&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%2Fk79z35zyknboxanegkd9.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%2Fk79z35zyknboxanegkd9.png" alt="Five-level secrets management maturity ladder from ad hoc to governed lifecycle" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Five Levels at a Glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Level&lt;/th&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;Storage&lt;/th&gt;
&lt;th&gt;Delivery&lt;/th&gt;
&lt;th&gt;Lifecycle&lt;/th&gt;
&lt;th&gt;Evidence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Ad hoc&lt;/td&gt;
&lt;td&gt;Code, chat, local files&lt;/td&gt;
&lt;td&gt;Copy-paste&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Centralized&lt;/td&gt;
&lt;td&gt;Vault, Secrets Manager, Parameter Store&lt;/td&gt;
&lt;td&gt;Manual fetch, broad IAM&lt;/td&gt;
&lt;td&gt;Rare manual rotation&lt;/td&gt;
&lt;td&gt;Partial access logs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Guarded&lt;/td&gt;
&lt;td&gt;Central store + no secrets in git&lt;/td&gt;
&lt;td&gt;CI injects at runtime; least privilege emerging&lt;/td&gt;
&lt;td&gt;Scheduled rotation for critical secrets&lt;/td&gt;
&lt;td&gt;Scanner + access logs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Short-lived&lt;/td&gt;
&lt;td&gt;Central store + workload identity&lt;/td&gt;
&lt;td&gt;OIDC, IAM roles, dynamic secrets&lt;/td&gt;
&lt;td&gt;Automatic expiry by design&lt;/td&gt;
&lt;td&gt;CloudTrail, session attribution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Governed&lt;/td&gt;
&lt;td&gt;Tiered stores by sensitivity&lt;/td&gt;
&lt;td&gt;Zero standing privilege where possible&lt;/td&gt;
&lt;td&gt;Automated rotation + break-glass&lt;/td&gt;
&lt;td&gt;Audit-ready reports, MTTR metrics&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%2F1ou7j0mhcln9a6aov5ts.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%2F1ou7j0mhcln9a6aov5ts.png" alt="Secrets lifecycle flow from blocked git commits to rotation and audit evidence" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Level 1: Ad Hoc Secrets
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;You are here if:&lt;/strong&gt; passwords and API keys live in source code, wiki pages, ticket comments, or shared drives, and nobody can list every production credential.&lt;/p&gt;

&lt;h3&gt;
  
  
  Typical signals
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;.env&lt;/code&gt; files committed to git (current or historical)&lt;/li&gt;
&lt;li&gt;"Ask Sarah for the prod key" is a documented onboarding step&lt;/li&gt;
&lt;li&gt;No named owner for database or cloud credentials&lt;/li&gt;
&lt;li&gt;The offboarding checklist does not mention secret revocation&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Risk profile
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Time to revoke after departure:&lt;/strong&gt; unknown or days&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blast radius of one leak:&lt;/strong&gt; entire environment&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit answer to "who accessed this secret?":&lt;/strong&gt; shrug&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Anti-patterns you will recognize
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The onboarding packet:&lt;/strong&gt; a Google Doc titled "Prod Credentials" shared with every new hire.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The demo that became production:&lt;/strong&gt; a Stripe test key hardcoded in a tutorial branch that merged years ago.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The hero key:&lt;/strong&gt; one senior engineer's personal AWS access key wired into cron jobs because "it was faster than fixing IAM."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these fails because the team is careless. They fail because there is no model for what "better" looks like.&lt;/p&gt;

&lt;h3&gt;
  
  
  What to fix first (exit criteria for Level 1)
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Inventory every known production secret (spreadsheet is fine for week one).&lt;/li&gt;
&lt;li&gt;Assign an &lt;strong&gt;owner&lt;/strong&gt; per secret class (database, cloud, third-party API).&lt;/li&gt;
&lt;li&gt;Remove secrets from the latest code branch, not git history yet; stop the bleeding first.&lt;/li&gt;
&lt;li&gt;Run a secret scanner on the default branch and open issues for findings.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You are ready for Level 2 when no new secrets are added to git and every production credential has an owner.&lt;/p&gt;

&lt;h2&gt;
  
  
  Level 2: Centralized Storage
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;You are here if:&lt;/strong&gt; secrets moved into AWS Secrets Manager, Systems Manager (SSM) Parameter Store, HashiCorp Vault, or similar, but humans still copy values manually, and IAM policies are wider than necessary.&lt;/p&gt;

&lt;h3&gt;
  
  
  Typical signals
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Central store exists; developers still export secrets to local &lt;code&gt;.env&lt;/code&gt; for convenience&lt;/li&gt;
&lt;li&gt;CI reads long-lived keys from the store, but stores them as pipeline variables duplicated everywhere&lt;/li&gt;
&lt;li&gt;Rotation is possible in the product, but not enabled&lt;/li&gt;
&lt;li&gt;Staging and production share the same credentials "to save time"&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What good looks like at Level 2
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;One approved store per environment class (non-prod vs prod)&lt;/li&gt;
&lt;li&gt;Read access scoped by role, not by individual IAM users with access keys&lt;/li&gt;
&lt;li&gt;Documentation: which app reads which secret path&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Example: store a database URL in AWS Secrets Manager
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws secretsmanager create-secret &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"/prod/myapp/database-url"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--description&lt;/span&gt; &lt;span class="s2"&gt;"Production database connection string"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--secret-string&lt;/span&gt; &lt;span class="s2"&gt;"postgresql://user:PASSWORD@host:5432/dbname"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Restrict read with an IAM policy on the application role, not on a shared developer user:&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;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ReadAppDatabaseSecret"&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;"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;"secretsmanager:GetSecretValue"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:secretsmanager:REGION:ACCOUNT_ID:secret:/prod/myapp/database-url-*"&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;
  
  
  Exit criteria for Level 2
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Zero net-new secrets in repositories (enforced by habit, not yet by CI gate)&lt;/li&gt;
&lt;li&gt;Production secrets exist only in the approved store&lt;/li&gt;
&lt;li&gt;Each secret path has an owner and a documented consumer list&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Parameter Store vs Secrets Manager (quick choice)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Need&lt;/th&gt;
&lt;th&gt;Start here&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Simple config values, non-rotating strings&lt;/td&gt;
&lt;td&gt;SSM Parameter Store (&lt;code&gt;SecureString&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database passwords with automatic rotation&lt;/td&gt;
&lt;td&gt;AWS Secrets Manager&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-account secret sharing with audit&lt;/td&gt;
&lt;td&gt;Secrets Manager + resource policy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;On-prem or multi-cloud with dynamic DB creds&lt;/td&gt;
&lt;td&gt;HashiCorp Vault&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For many Level 2 teams on AWS, &lt;strong&gt;SSM for config&lt;/strong&gt; and &lt;strong&gt;Secrets Manager for credentials that rotate&lt;/strong&gt; is enough. Do not delay Level 2 while debating Vault versus cloud-native stores.&lt;/p&gt;

&lt;h2&gt;
  
  
  Level 3: Guarded Delivery
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;You are here if:&lt;/strong&gt; secrets rarely enter git because &lt;strong&gt;controls enforce it&lt;/strong&gt;, rotation is scheduled for high-risk credentials, and CI/CD injects secrets at runtime instead of baking them into images.&lt;/p&gt;

&lt;h3&gt;
  
  
  Typical signals
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Pre-commit or pull request secret scanning blocks merges&lt;/li&gt;
&lt;li&gt;CI pulls secrets from the store during deploy, not from static variables checked into the pipeline repo&lt;/li&gt;
&lt;li&gt;Critical secrets (database admin, cloud root-adjacent keys) have a rotation date&lt;/li&gt;
&lt;li&gt;Separate credentials for staging and production&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  How to add a merge gate with Gitleaks in GitHub Actions
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;secret-scan&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;gitleaks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;fetch-depth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gitleaks/gitleaks-action@v2&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;GITHUB_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pair scanning with a shared allowlist (&lt;code&gt;.gitleaks.toml&lt;/code&gt;) so test fixtures do not train people to ignore alerts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rotation calendar (minimum viable)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Secret type&lt;/th&gt;
&lt;th&gt;Maximum age&lt;/th&gt;
&lt;th&gt;Owner action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Database admin&lt;/td&gt;
&lt;td&gt;90 days&lt;/td&gt;
&lt;td&gt;Automated or ticketed rotation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Third-party API (billing, email)&lt;/td&gt;
&lt;td&gt;90 days&lt;/td&gt;
&lt;td&gt;Owner rotates + updates store&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI deploy keys (if still required)&lt;/td&gt;
&lt;td&gt;30 days&lt;/td&gt;
&lt;td&gt;Prefer elimination over rotation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TLS private keys&lt;/td&gt;
&lt;td&gt;Before cert expiry&lt;/td&gt;
&lt;td&gt;Automated renewal where possible&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Exit criteria for Level 3
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Pull requests with new secrets fail CI&lt;/li&gt;
&lt;li&gt;No standing production keys on developer laptops for routine work&lt;/li&gt;
&lt;li&gt;Rotation tickets exist and close on schedule for tier-1 secrets&lt;/li&gt;
&lt;li&gt;Incident runbook includes "revoke and rotate" as step one, not "delete git history" alone&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  How Level 3 maps to compliance language
&lt;/h3&gt;

&lt;p&gt;Auditors rarely ask "What maturity level are you?" They ask for controls. Level 3 typically satisfies early evidence for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Access control:&lt;/strong&gt; secrets not in source code; store access IAM-scoped&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Change management:&lt;/strong&gt; PR gates show who approved code without embedded credentials&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logging:&lt;/strong&gt; central store access logs retained&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You still need policy documents and owners. The model tells you which &lt;strong&gt;technical artifacts&lt;/strong&gt; to produce.&lt;/p&gt;

&lt;h2&gt;
  
  
  Level 4: Short-Lived Credentials
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;You are here if:&lt;/strong&gt; applications and pipelines &lt;strong&gt;assume roles&lt;/strong&gt; or exchange identity tokens instead of reading long-lived API keys. Secrets expire by design. Session attribution appears in logs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Typical signals
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;GitHub Actions uses OpenID Connect (OIDC) to assume AWS IAM roles, no &lt;code&gt;AWS_ACCESS_KEY_ID&lt;/code&gt; in GitHub secrets for deploy&lt;/li&gt;
&lt;li&gt;EC2, ECS, Lambda, and Kubernetes workloads use instance/task/pod identity, not embedded keys&lt;/li&gt;
&lt;li&gt;Database access uses IAM authentication or credentials with a TTL under 24 hours, where supported&lt;/li&gt;
&lt;li&gt;Dynamic secrets (Vault database engine, AWS STS) appear in architecture diagrams&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Example: GitHub Actions OIDC to AWS (pattern)
&lt;/h3&gt;

&lt;p&gt;Trust policy on the deployment role (simplified):&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::ACCOUNT_ID: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="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;"StringLike"&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: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:ORG/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;p&gt;Workflow step:&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;id-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&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;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws-actions/configure-aws-credentials@v4&lt;/span&gt;
    &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;role-to-assume&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;arn:aws:iam::ACCOUNT_ID:role/github-deploy-prod&lt;/span&gt;
      &lt;span class="na"&gt;aws-region&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;eu-west-1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pipeline never stores static AWS keys in GitHub.&lt;/p&gt;

&lt;h3&gt;
  
  
  What changes in threat model
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Leaked CI logs no longer contain permanent cloud credentials&lt;/li&gt;
&lt;li&gt;A compromised laptop does not equal compromised production API access&lt;/li&gt;
&lt;li&gt;Revocation means "end the session" plus policy change, not hunt for copied keys&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Exit criteria for Level 4
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Tier-1 systems have no long-lived cloud access keys in CI&lt;/li&gt;
&lt;li&gt;Workloads use identity-based access for cloud APIs where available&lt;/li&gt;
&lt;li&gt;CloudTrail (or equivalent) shows &lt;strong&gt;who assumed which role&lt;/strong&gt; for deploy paths&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Level 5: Governed Lifecycle
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;You are here if:&lt;/strong&gt; secret management is measurable, audited, and resilient. Rotation is automated where possible. Break-glass exists but is rare, logged, and reviewed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Typical signals
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Automated rotation enabled in Secrets Manager or Vault for supported secret types&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Break-glass&lt;/strong&gt; credentials are sealed, monitored, and used only in emergencies&lt;/li&gt;
&lt;li&gt;Mean time to revoke (MTTR) after offboarding or leak is tracked and under team SLO&lt;/li&gt;
&lt;li&gt;Compliance evidence (SOC 2, ISO 27001) pulls from logs, not screenshots&lt;/li&gt;
&lt;li&gt;Secret sprawl reviews happen quarterly; unused secrets are deleted&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Break-glass principles
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Separate break-glass from daily admin roles.&lt;/li&gt;
&lt;li&gt;Require MFA and ticket reference for use.&lt;/li&gt;
&lt;li&gt;Alert on every break-glass authentication.&lt;/li&gt;
&lt;li&gt;Review usage in post-incident or weekly security sync.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Evidence pipeline (what auditors ask for)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Control&lt;/th&gt;
&lt;th&gt;Evidence source&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Secrets not in source code&lt;/td&gt;
&lt;td&gt;Gitleaks CI history + clean default-branch scan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Access limited by role&lt;/td&gt;
&lt;td&gt;IAM policy export + role assumption logs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rotation&lt;/td&gt;
&lt;td&gt;Secrets Manager rotation Lambda logs or Vault audit log&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Offboarding&lt;/td&gt;
&lt;td&gt;HR date vs IAM disable date vs secret version deprecation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Exit criteria for Level 5
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Documented SLO: for example, "revoke all access within 4 hours of termination"&lt;/li&gt;
&lt;li&gt;Automated rotation for ≥80% of tier-1 secrets that support it&lt;/li&gt;
&lt;li&gt;Quarterly access review with deletion of unused secrets&lt;/li&gt;
&lt;li&gt;One successful tabletop exercise: leaked key → rotate → verify blast radius contained&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Metrics worth tracking at Level 5
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Why it matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mean time to revoke (MTTR) after offboarding&lt;/td&gt;
&lt;td&gt;Proves HR and IAM stay in sync&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mean time to rotate after scanner alert&lt;/td&gt;
&lt;td&gt;Proves alerts are not cosmetic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Count of active IAM access keys older than 90 days&lt;/td&gt;
&lt;td&gt;Surfaces Level 4 debt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secrets with no read in 90 days&lt;/td&gt;
&lt;td&gt;Candidates for deletion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Break-glass uses per quarter&lt;/td&gt;
&lt;td&gt;Should be near zero; spikes warrant review&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  How to Assess Your Current Level
&lt;/h2&gt;

&lt;p&gt;Run this workshop in 60 minutes. Score &lt;strong&gt;1&lt;/strong&gt; (no), &lt;strong&gt;2&lt;/strong&gt; (partial), &lt;strong&gt;3&lt;/strong&gt; (yes) for each row. Average per level. Your floor level is the lowest level where any critical row scores &lt;strong&gt;1&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Level 1 indicators
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;1&lt;/th&gt;
&lt;th&gt;2&lt;/th&gt;
&lt;th&gt;3&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Are production secrets committed to git today?&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Can you list all production API keys in one document?&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Does offboarding include credential revocation?&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Level 2 indicators
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;1&lt;/th&gt;
&lt;th&gt;2&lt;/th&gt;
&lt;th&gt;3&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Is there one approved secret store per environment?&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Do apps fetch secrets at runtime (not build time in images)?&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Are IAM policies scoped to secret paths, not &lt;code&gt;*&lt;/code&gt;?&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Level 3 indicators
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;1&lt;/th&gt;
&lt;th&gt;2&lt;/th&gt;
&lt;th&gt;3&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Do PRs fail when scanners find new secrets?&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Are staging and prod credentials separate?&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Do tier-1 secrets rotate on schedule?&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Level 4 indicators
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;1&lt;/th&gt;
&lt;th&gt;2&lt;/th&gt;
&lt;th&gt;3&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Does CI use OIDC/roles instead of static cloud keys?&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Do workloads use instance/task identity for cloud APIs?&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Can you attribute deploy actions to a role session in logs?&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Level 5 indicators
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;1&lt;/th&gt;
&lt;th&gt;2&lt;/th&gt;
&lt;th&gt;3&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Is rotation automated for supported secret types?&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Is break-glass defined, monitored, and reviewed?&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Do you track time-to-revoke after incidents?&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Verification signal:&lt;/strong&gt; If two teams score themselves Level 4 but only one uses OIDC for CI, trust logs, not self-assessment slides.&lt;/p&gt;

&lt;h3&gt;
  
  
  One-hour workshop agenda (copy for your team)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Minutes&lt;/th&gt;
&lt;th&gt;Activity&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0–10&lt;/td&gt;
&lt;td&gt;Define scope: which systems are in-scope for "production secrets"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10–25&lt;/td&gt;
&lt;td&gt;Silent scoring: each lead fills indicator tables independently&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;25–40&lt;/td&gt;
&lt;td&gt;Compare scores; argue with evidence (logs, scans), not opinions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;40–50&lt;/td&gt;
&lt;td&gt;Agree on effective level and pick &lt;strong&gt;one&lt;/strong&gt; exit criterion for next sprint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50–60&lt;/td&gt;
&lt;td&gt;Assign owner, due date, and verification command (for example, &lt;code&gt;gitleaks detect&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Publish notes internally. Re-run quarterly. Maturity without remeasurement becomes a poster.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Move Up One Level at a Time
&lt;/h2&gt;

&lt;p&gt;Do not jump from Level 1 to Level 5 in one quarter. Sequence work that sticks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Level 1 → 2
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Secret inventory workshop.&lt;/li&gt;
&lt;li&gt;Pick one store: AWS Secrets Manager, SSM Parameter Store (SecureString), or Vault.&lt;/li&gt;
&lt;li&gt;Migrate the top five highest-risk secrets.&lt;/li&gt;
&lt;li&gt;Delete local copies from laptops after migration.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Level 2 → 3
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Deploy three-layer secret scanning (local, PR, default branch), see related Gitleaks article.&lt;/li&gt;
&lt;li&gt;Remove secrets from CI YAML; inject from store at deploy time.&lt;/li&gt;
&lt;li&gt;Split staging and production credentials.&lt;/li&gt;
&lt;li&gt;Put rotation on the calendar for database and payment API keys.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Level 3 → 4
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Replace GitHub static AWS keys with OIDC federation.&lt;/li&gt;
&lt;li&gt;Move EC2/ECS workloads to IAM roles (no access keys on instances).&lt;/li&gt;
&lt;li&gt;Enable CloudTrail log file validation and alert on &lt;code&gt;CreateAccessKey&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Deprecate one long-lived key per sprint until none remain for tier-1 paths.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Level 4 → 5 (ongoing)
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Enable automatic rotation where the datastore supports it.&lt;/li&gt;
&lt;li&gt;Define break-glass; run one drill.&lt;/li&gt;
&lt;li&gt;Build evidence exports for your compliance framework.&lt;/li&gt;
&lt;li&gt;Set MTTR SLO and review monthly.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  How to Map Tools to Maturity Levels
&lt;/h2&gt;

&lt;p&gt;Tools do not equal maturity. They &lt;strong&gt;enable&lt;/strong&gt; it when the process matches.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Typical starting level&lt;/th&gt;
&lt;th&gt;What it unlocks&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;.env&lt;/code&gt; + gitignore only&lt;/td&gt;
&lt;td&gt;Level 1 (fragile)&lt;/td&gt;
&lt;td&gt;Local dev convenience not production control&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS SSM Parameter Store (SecureString)&lt;/td&gt;
&lt;td&gt;Level 2&lt;/td&gt;
&lt;td&gt;Central storage, IAM-scoped access&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS Secrets Manager&lt;/td&gt;
&lt;td&gt;Level 2–5&lt;/td&gt;
&lt;td&gt;Storage, rotation Lambda, audit integration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HashiCorp Vault&lt;/td&gt;
&lt;td&gt;Level 2–5&lt;/td&gt;
&lt;td&gt;Dynamic secrets, PKI, advanced policies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gitleaks / GitHub secret scanning&lt;/td&gt;
&lt;td&gt;Level 3&lt;/td&gt;
&lt;td&gt;Merge gates, hygiene scans&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub OIDC + cloud IAM&lt;/td&gt;
&lt;td&gt;Level 4&lt;/td&gt;
&lt;td&gt;Short-lived CI credentials&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS IAM Roles Anywhere / workload identity&lt;/td&gt;
&lt;td&gt;Level 4&lt;/td&gt;
&lt;td&gt;Non-AWS workload federation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SIEM / CloudTrail alerts&lt;/td&gt;
&lt;td&gt;Level 5&lt;/td&gt;
&lt;td&gt;Evidence and anomaly detection&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Common mistake:&lt;/strong&gt; buying Vault (Level 2–5 tool) while teams still email secrets (Level 1 behavior). Fix behavior and inventory first.&lt;/p&gt;

&lt;h3&gt;
  
  
  Kubernetes and container notes
&lt;/h3&gt;

&lt;p&gt;Container platforms often stall at Level 2–3 because secrets become Kubernetes &lt;code&gt;Secret&lt;/code&gt; objects mounted as files, better than git, but still long-lived and often over-shared across namespaces.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Practice&lt;/th&gt;
&lt;th&gt;Maturity lift&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;External Secrets Operator pulling from Vault or Secrets Manager&lt;/td&gt;
&lt;td&gt;Level 3 delivery&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Separate namespaces and RBAC per environment&lt;/td&gt;
&lt;td&gt;Level 3 isolation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Workload identity (IRSA on EKS, GKE workload identity) instead of static cloud keys in secrets&lt;/td&gt;
&lt;td&gt;Level 4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sealed Secrets or SOPS for git-stored encrypted manifests&lt;/td&gt;
&lt;td&gt;Level 3 bridge not a substitute for rotation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If your roadmap includes Kubernetes, treat cluster secrets as a &lt;strong&gt;delivery mechanism&lt;/strong&gt;, not a &lt;strong&gt;maturity destination&lt;/strong&gt;. The level is defined by lifetime, scope, and evidence, not by whether the YAML type is &lt;code&gt;Secret&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Verify Progress
&lt;/h2&gt;

&lt;p&gt;Use observable checks, not maturity posters on the wall.&lt;/p&gt;

&lt;h3&gt;
  
  
  Quick technical checks
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Historical secrets in git (example with gitleaks)&lt;/span&gt;
gitleaks detect &lt;span class="nt"&gt;--source&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--verbose&lt;/span&gt;

&lt;span class="c"&gt;# List IAM users with access keys (AWS CLI)&lt;/span&gt;
aws iam generate-credential-report
aws iam get-credential-report &lt;span class="nt"&gt;--output&lt;/span&gt; text | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-f1&lt;/span&gt;,9,11,14
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Interpretation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Gitleaks findings on the default branch → below Level 3 until resolved and prevented&lt;/li&gt;
&lt;li&gt;IAM users with active access keys used by automation → below Level 4 for those paths&lt;/li&gt;
&lt;li&gt;No rotation events in Secrets Manager for 12+ months → below Level 5 for those secrets&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Process checks
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Offboarding ticket includes secret revocation, closed before the access removal ticket closes.&lt;/li&gt;
&lt;li&gt;New service checklist includes "no secrets in image" and "identity-based cloud access."&lt;/li&gt;
&lt;li&gt;Incident runbook step one: rotate and revoke, not only "remove from git."&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  When This Breaks Down
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Maturity cosplay:&lt;/strong&gt; central store exists, but every team still keeps a backup &lt;code&gt;.env&lt;/code&gt; "just in case."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scanner fatigue:&lt;/strong&gt; allowlists grow until real leaks merge. Tune rules; do not disable gates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rotation without revocation:&lt;/strong&gt; new secret version exists; old keys still work everywhere.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero standing privilege everywhere day one:&lt;/strong&gt; teams bypass controls with shared admin "temporarily" for years. Sequence Level 4 by workload tier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-cloud sprawl:&lt;/strong&gt; Each cloud has a different model. Pick a &lt;strong&gt;logical&lt;/strong&gt; maturity target per environment, not one global tool mandate.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Objections you will hear (and how to answer them)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;"Rotation will cause an outage."&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Start with non-production secrets and one low-risk production credential. Automate rollback in the store. Outages come from surprise dependencies, not rotation itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"OIDC is too complex for our small team."&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
One GitHub Actions deploy path to one AWS role is a weekend project. That single path often removes the highest-risk static key.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Developers need local prod access to debug."&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Grant break-glass with logging, not shared prod keys on laptops. Level 5 is not "no access"; it is &lt;strong&gt;attributed, time-bound access&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"We passed the penetration test; we are fine."&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Point-in-time tests do not prove daily hygiene. Maturity is measured in continuous signals: scanners, logs, and rotation events.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: Is Level 5 realistic for a ten-person startup?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You can reach Level 4 patterns (OIDC, roles, scanning) on a small team. Level 5 evidence automation often grows with compliance pressure. Aim for Level 4 with one or two Level 5 practices (break-glass, MTTR tracking).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Vault or AWS Secrets Manager?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you are AWS-primary, start with Secrets Manager and SSM. Add Vault when you need dynamic database credentials, multi-cloud abstraction, or complex policy workflows. Maturity level matters more than brand.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Does deleting secrets from git history count as maturity?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It is a cleanup, not control. Rotation and revocation contain compromise. History rewrite without rotation leaves active credentials in the attacker's hands.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How does this relate to the software development lifecycle (SDLC)?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Level 3 maps to shift-left detection in CI. Level 4 maps to secure deployment and runtime identity. Level 5 maps to operations, audit, and incident response. The model spans the full SDLC—not just production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Can we stay at Level 3 forever?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Many regulated teams have been doing so for years. Level 4 becomes urgent when CI keys leak, auditors ask for session attribution, or you scale past one cloud account.&lt;/p&gt;

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

&lt;p&gt;In this article, you learned a five-level secrets management maturity model for DevSecOps teams: from ad hoc credentials in chat and code, through centralized storage and guarded CI delivery, to short-lived identity and governed lifecycle with audit evidence.&lt;/p&gt;

&lt;p&gt;You can assess your team with the indicator tables, move up one level at a time, and verify progress with scanners, IAM reports, and rotation logs, not slide decks.&lt;/p&gt;

&lt;p&gt;Your next step: run the 60-minute assessment workshop, pick the &lt;strong&gt;lowest-scoring level&lt;/strong&gt;, and close one exit criterion this sprint. Maturity compounds when storage, delivery, lifecycle, and evidence move together.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html" rel="noopener noreferrer"&gt;OWASP Secrets Management Cheat Sheet&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://csrc.nist.gov/publications/detail/sp/800-57-part-1/rev-5/final" rel="noopener noreferrer"&gt;NIST SP 800-57 - Recommendation for Key Management&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/secretsmanager/latest/userguide/intro.html" rel="noopener noreferrer"&gt;AWS Secrets Manager documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles.html" rel="noopener noreferrer"&gt;AWS IAM roles&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services" rel="noopener noreferrer"&gt;GitHub Actions - OpenID Connect with AWS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.hashicorp.com/vault/docs" rel="noopener noreferrer"&gt;HashiCorp Vault documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/gitleaks/gitleaks" rel="noopener noreferrer"&gt;Gitleaks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.cisecurity.org/controls/data-protection" rel="noopener noreferrer"&gt;CIS Controls - Control 3: Data Protection&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devsecops</category>
      <category>security</category>
      <category>secrets</category>
      <category>aws</category>
    </item>
    <item>
      <title>How to Build a Security Alert Router with n8n</title>
      <dc:creator>Ilyas Rufai</dc:creator>
      <pubDate>Sun, 28 Jun 2026 06:23:43 +0000</pubDate>
      <link>https://dev.to/rufilboss/my-first-devsecops-automation-in-n8n-route-security-alerts-to-slack-27h</link>
      <guid>https://dev.to/rufilboss/my-first-devsecops-automation-in-n8n-route-security-alerts-to-slack-27h</guid>
      <description>&lt;p&gt;I recently watched a 20-minute n8n intro video on the n8n official page and wanted to implement it in a real DevSecOps project. Then I remember one issue I've been seeing on a small team, over time: a CI scanner finds a secret, Dependabot opens a low-severity advisory, and by Friday, nobody knows which channel is the source of truth for security signals.&lt;/p&gt;

&lt;p&gt;Meanwhile, security tools are only useful when findings reach humans &lt;strong&gt;fast&lt;/strong&gt;, &lt;strong&gt;consistently&lt;/strong&gt;, and &lt;strong&gt;at the right urgency&lt;/strong&gt;. I did not need a full security orchestration platform on day one. I needed a small automation layer that normalizes alerts and routes them by severity.&lt;/p&gt;

&lt;p&gt;In this article, I document how I built my first DevSecOps workflow in &lt;a href="https://n8n.io/" rel="noopener noreferrer"&gt;n8n&lt;/a&gt;: a &lt;strong&gt;security alert router&lt;/strong&gt; that accepts a webhook payload, filters by severity, and posts to the correct Slack channel. I tested it with &lt;code&gt;curl&lt;/code&gt; first and left a clear path to swap the webhook source for GitHub Actions or a scanner later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who might find this useful:&lt;/strong&gt; Engineers new to n8n who want a practical DevSecOps starter project with a clear path to more advanced automations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I built:&lt;/strong&gt; A six-node workflow; Webhook → Normalize → IF (severity) → Slack (urgent or log) → Respond.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I used:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Docker Compose to run n8n locally&lt;/li&gt;
&lt;li&gt;A Slack workspace with a bot and two channels (&lt;code&gt;#security-alerts&lt;/code&gt;, &lt;code&gt;#security-log&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Sample JSON payloads and &lt;code&gt;curl&lt;/code&gt; for testing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Related work:&lt;/strong&gt; I hardened the repo side first with &lt;a href="https://dev.to/rufilboss/secret-scanning-in-ci-what-pre-commit-pull-request-and-main-branch-each-actually-catch-4k99"&gt;Secret Scanning in CI: Three Layers with Gitleaks&lt;/a&gt;. This n8n workflow is the notification layer on top.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;I defined a &lt;strong&gt;standard JSON payload&lt;/strong&gt; for security findings so any tool can POST to one webhook.&lt;/li&gt;
&lt;li&gt;I used n8n to &lt;strong&gt;normalize&lt;/strong&gt; fields, then &lt;strong&gt;route&lt;/strong&gt; HIGH/CRITICAL to &lt;code&gt;#security-alerts&lt;/code&gt; and everything else to &lt;code&gt;#security-log&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;I ran n8n locally with Docker on port &lt;code&gt;5678&lt;/code&gt; and tested with sample JSON before touching real CI webhooks.&lt;/li&gt;
&lt;li&gt;I exported the workflow JSON, attached Slack credentials, activated it, and verified with two &lt;code&gt;curl&lt;/code&gt; calls.&lt;/li&gt;
&lt;li&gt;My next step: replace the test webhook with GitHub &lt;code&gt;repository_vulnerability_alert&lt;/code&gt; events or a Gitleaks CI callback.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why I did not stop at "check the tool dashboard"
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;What works&lt;/th&gt;
&lt;th&gt;What breaks&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Each engineer watches their own scanner UI&lt;/td&gt;
&lt;td&gt;Fine for solo projects&lt;/td&gt;
&lt;td&gt;Findings get missed during on-call handoffs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Email alerts from every tool&lt;/td&gt;
&lt;td&gt;Central inbox&lt;/td&gt;
&lt;td&gt;Alert fatigue; no severity routing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One Slack channel for everything&lt;/td&gt;
&lt;td&gt;Simple setup&lt;/td&gt;
&lt;td&gt;LOW noise hides CRITICAL incidents&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom script per integration&lt;/td&gt;
&lt;td&gt;Full control&lt;/td&gt;
&lt;td&gt;Unmaintained scripts; no visual debugging&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; A thin automation layer with &lt;strong&gt;one inbound contract&lt;/strong&gt; and &lt;strong&gt;severity-based routing&lt;/strong&gt; gives observability without building a full SOAR (security orchestration, automation, and response) platform in week one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Security tool / curl test
        |
        | POST JSON (webhook)
        v
   n8n: Normalize Finding
        |
        v
   n8n: HIGH or CRITICAL?
      /        \
     v          v
Slack Urgent   Slack Log
      \        /
       v      v
   Respond 200 OK
&lt;/code&gt;&lt;/pre&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%2Foph1bqvqgbxereflfi72.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%2Foph1bqvqgbxereflfi72.png" alt="Security alert router workflow architecture in n8n" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Standard finding payload
&lt;/h3&gt;

&lt;p&gt;I picked one JSON shape every source should send Gitleaks, Dependabot, Trivy, or a manual &lt;code&gt;curl&lt;/code&gt; test:&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;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gitleaks"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"repo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"acme/webapp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"severity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HIGH"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AWS Access Key detected"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"detail"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"file: src/config.py line 42"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://github.com/acme/webapp/actions/runs/123456"&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;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;source&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Which tool raised the finding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;repo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Repository or service name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;severity&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;CRITICAL&lt;/code&gt;, &lt;code&gt;HIGH&lt;/code&gt;, &lt;code&gt;MEDIUM&lt;/code&gt;, &lt;code&gt;LOW&lt;/code&gt;, or &lt;code&gt;INFO&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;title&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Short human-readable summary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;detail&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Extra context (file path, CVE, rule ID)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;url&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Link to PR, workflow run, or ticket&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Step 1 — I ran n8n locally with Docker
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;articles/assets/n8n-security-alert-router-slack
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I opened &lt;code&gt;http://localhost:5678&lt;/code&gt; and created my owner account on first launch.&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%2Fhe6rd6kh1gfuzj7udn9m.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%2Fhe6rd6kh1gfuzj7udn9m.png" alt="n8n welcome" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verification:&lt;/strong&gt; &lt;code&gt;docker compose ps&lt;/code&gt; shows the &lt;code&gt;n8n&lt;/code&gt; container running and port &lt;code&gt;5678&lt;/code&gt; mapped.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2 — I created a Slack app and bot token
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://api.slack.com/apps" rel="noopener noreferrer"&gt;api.slack.com/apps&lt;/a&gt; → &lt;strong&gt;Create New App&lt;/strong&gt; → &lt;strong&gt;From scratch&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Under &lt;strong&gt;OAuth &amp;amp; Permissions&lt;/strong&gt;, I added bot scopes: &lt;code&gt;chat:write&lt;/code&gt;, &lt;code&gt;chat:write.public&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Install to Workspace&lt;/strong&gt; → copied the &lt;strong&gt;Bot User OAuth Token&lt;/strong&gt; (&lt;code&gt;xoxb-...&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Created channels &lt;code&gt;#security-alerts&lt;/code&gt; and &lt;code&gt;#security-log&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Invited the bot: &lt;code&gt;/invite @YourBotName&lt;/code&gt; in each channel&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In n8n: &lt;strong&gt;Credentials&lt;/strong&gt; → &lt;strong&gt;New&lt;/strong&gt; → &lt;strong&gt;Slack API&lt;/strong&gt; → pasted the bot token.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3 — I built the workflow
&lt;/h2&gt;

&lt;p&gt;I started by importing the JSON below, then tweaked channel names and expressions until executions went green.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option A: Import my workflow (fastest)
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;In n8n, &lt;strong&gt;Workflows&lt;/strong&gt; → &lt;strong&gt;Import from File&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Copy this and save it into a file on your pc; &lt;code&gt;example.json&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&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;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Security Alert Router"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"nodes"&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;"parameters"&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;"httpMethod"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"POST"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"security-finding"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"responseMode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"responseNode"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"options"&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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"a1b2c3d4-0001-4000-8000-000000000001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Webhook"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"n8n-nodes-base.webhook"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"typeVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"position"&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"webhookId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"security-finding-webhook"&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;"parameters"&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;"assignments"&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;"assignments"&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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"f1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"={{ $json.body.source ?? $json.source }}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"f2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"name"&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"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"={{ $json.body.repo ?? $json.repo }}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"f3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"severity"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"={{ ($json.body.severity ?? $json.severity ?? 'UNKNOWN').toUpperCase() }}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"f4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"={{ $json.body.title ?? $json.title }}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"f5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"detail"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"={{ $json.body.detail ?? $json.detail ?? 'No detail provided' }}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"f6"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"={{ $json.body.url ?? $json.url ?? '' }}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&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="nl"&gt;"options"&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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"a1b2c3d4-0002-4000-8000-000000000002"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Normalize Finding"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"n8n-nodes-base.set"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"typeVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;3.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"position"&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="mi"&gt;240&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;300&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;"parameters"&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;"conditions"&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;"options"&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;"caseSensitive"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"leftValue"&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;"typeValidation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"strict"&lt;/span&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="mi"&gt;2&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;"conditions"&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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"c1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"leftValue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"={{ $json.severity }}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"rightValue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HIGH"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"operator"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"operation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"equals"&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="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"c2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"leftValue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"={{ $json.severity }}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"rightValue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CRITICAL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"operator"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"operation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"equals"&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="nl"&gt;"combinator"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"or"&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;"options"&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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"a1b2c3d4-0003-4000-8000-000000000003"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"High or Critical?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"n8n-nodes-base.if"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"typeVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;2.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"position"&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="mi"&gt;480&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;300&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;"parameters"&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;"authentication"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"accessToken"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"select"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"channel"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"channelId"&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;"__rl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"#security-alerts"&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;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"=:rotating_light: *{{ $json.severity }}* finding from *{{ $json.source }}*&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;*Repo:* {{ $json.repo }}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;*Title:* {{ $json.title }}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;*Detail:* {{ $json.detail }}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;*Link:* {{ $json.url }}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"otherOptions"&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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"a1b2c3d4-0004-4000-8000-000000000004"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Slack Urgent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"n8n-nodes-base.slack"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"typeVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;2.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"position"&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="mi"&gt;760&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;180&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"credentials"&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;"slackApi"&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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"REPLACE_WITH_YOUR_CREDENTIAL_ID"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Slack account"&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="nl"&gt;"parameters"&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;"authentication"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"accessToken"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"select"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"channel"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"channelId"&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;"__rl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"#security-log"&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;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"=:information_source: *{{ $json.severity }}* finding logged from *{{ $json.source }}*&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;*Repo:* {{ $json.repo }}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;*Title:* {{ $json.title }}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;*Link:* {{ $json.url }}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"otherOptions"&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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"a1b2c3d4-0005-4000-8000-000000000005"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Slack Log"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"n8n-nodes-base.slack"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"typeVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;2.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"position"&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="mi"&gt;760&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;420&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"credentials"&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;"slackApi"&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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"REPLACE_WITH_YOUR_CREDENTIAL_ID"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Slack account"&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="nl"&gt;"parameters"&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;"respondWith"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"responseBody"&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="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;  &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;status&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;accepted&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;  &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;severity&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;{{ $('Normalize Finding').item.json.severity }}&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;  &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;routed&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;{{ $('High or Critical?').item.json.severity === 'HIGH' || $('High or Critical?').item.json.severity === 'CRITICAL' ? 'urgent' : 'log' }}&lt;/span&gt;&lt;span class="se"&gt;\"\n&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;"options"&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;"responseCode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"a1b2c3d4-0006-4000-8000-000000000006"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Respond to Webhook"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"n8n-nodes-base.respondToWebhook"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"typeVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"position"&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="mi"&gt;1040&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;300&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;"connections"&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;"Webhook"&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;"main"&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="nl"&gt;"node"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Normalize Finding"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"index"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&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="nl"&gt;"Normalize Finding"&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;"main"&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="nl"&gt;"node"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"High or Critical?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"index"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&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="nl"&gt;"High or Critical?"&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;"main"&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="nl"&gt;"node"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Slack Urgent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"index"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&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="nl"&gt;"node"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Slack Log"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"index"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&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="nl"&gt;"Slack Urgent"&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;"main"&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="nl"&gt;"node"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Respond to Webhook"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"index"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&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="nl"&gt;"Slack Log"&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;"main"&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="nl"&gt;"node"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Respond to Webhook"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"index"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&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;span class="nl"&gt;"pinData"&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;"settings"&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;"executionOrder"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"v1"&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;"staticData"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tags"&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;"triggerCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"meta"&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;"templateCredsSetupCompleted"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&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;ol&gt;
&lt;li&gt;Open each Slack node and attach the Slack credential&lt;/li&gt;
&lt;li&gt;Confirm channel names match the workspace (&lt;code&gt;#security-alerts&lt;/code&gt;, &lt;code&gt;#security-log&lt;/code&gt;)&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Option B: Build node by node (how I learned n8n)
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Node 1: Webhook
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;HTTP Method:&lt;/strong&gt; POST&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Path:&lt;/strong&gt; &lt;code&gt;security-finding&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Respond:&lt;/strong&gt; Using &lt;strong&gt;Respond to Webhook&lt;/strong&gt; node&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I copied the &lt;strong&gt;Test URL&lt;/strong&gt;, n8n shows (production URL after activation).&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%2Fbq6wdpimayqt9amrhzge.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%2Fbq6wdpimayqt9amrhzge.png" alt="webhook-node-config" width="799" height="285"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Node 2: Normalize Finding (Set node)
&lt;/h4&gt;

&lt;p&gt;I mapped incoming body fields to a flat structure:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Expression&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;source&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{{ $json.body.source ?? $json.source }}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;repo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{{ $json.body.repo ?? $json.repo }}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;severity&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{{ ($json.body.severity ?? $json.severity ?? 'UNKNOWN').toUpperCase() }}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;title&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{{ $json.body.title ?? $json.title }}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;detail&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{{ $json.body.detail ?? $json.detail ?? 'No detail provided' }}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;url&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{{ $json.body.url ?? $json.url ?? '' }}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;??&lt;/code&gt; fallbacks let me test with raw JSON (&lt;code&gt;curl&lt;/code&gt;) or wrapped webhook bodies.&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%2Fyiki18dapjho8gjtqebs.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%2Fyiki18dapjho8gjtqebs.png" alt="set-normalize-node" width="800" height="372"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Node 3: High or Critical? (IF node)
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Combinator:&lt;/strong&gt; OR&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Condition 1:&lt;/strong&gt; &lt;code&gt;{{ $json.severity }}&lt;/code&gt; equals &lt;code&gt;HIGH&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Condition 2:&lt;/strong&gt; &lt;code&gt;{{ $json.severity }}&lt;/code&gt; equals &lt;code&gt;CRITICAL&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;True&lt;/strong&gt; branch → urgent Slack. &lt;strong&gt;False&lt;/strong&gt; branch → log Slack.&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%2Fkevkf2pi9wp8ac3q6543.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%2Fkevkf2pi9wp8ac3q6543.png" alt="if-severity-node" width="800" height="372"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Node 4a: Slack Urgent (&lt;code&gt;#security-alerts&lt;/code&gt;)
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;:rotating_light: *{{ $json.severity }}* finding from *{{ $json.source }}*
*Repo:* {{ $json.repo }}
*Title:* {{ $json.title }}
*Detail:* {{ $json.detail }}
*Link:* {{ $json.url }}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Node 4b: Slack Log (&lt;code&gt;#security-log&lt;/code&gt;)
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;:information_source: *{{ $json.severity }}* finding logged from *{{ $json.source }}*
*Repo:* {{ $json.repo }}
*Title:* {{ $json.title }}
*Link:* {{ $json.url }}
&lt;/code&gt;&lt;/pre&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%2Frd58lmox61hpfaemnldz.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%2Frd58lmox61hpfaemnldz.png" alt="slack-urgent-message" width="800" height="372"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Node 5: Respond to Webhook
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Respond With:&lt;/strong&gt; JSON&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Response Code:&lt;/strong&gt; &lt;code&gt;200&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Body:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"accepted"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"severity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{{ $('Normalize Finding').item.json.severity }}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"routed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"urgent-or-log"&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;I wired both Slack nodes into this responder.&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%2Fg75o9lpx37kfae0eea8y.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%2Fg75o9lpx37kfae0eea8y.png" alt="full-workflow-canvas" width="799" height="418"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4 — I activated and tested with curl
&lt;/h2&gt;

&lt;p&gt;I toggled the workflow &lt;strong&gt;Active&lt;/strong&gt; and used the &lt;strong&gt;Production Webhook URL&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  HIGH test (expected: &lt;code&gt;#security-alerts&lt;/code&gt;)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;WEBHOOK_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:5678/webhook/security-finding"&lt;/span&gt;

curl &lt;span class="nt"&gt;-sS&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$WEBHOOK_URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; @articles/assets/n8n-security-alert-router-slack/samples/high-severity.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected HTTP response:&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="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"accepted"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"severity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"HIGH"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"routed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"urgent"&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;
  
  
  LOW test (expected: &lt;code&gt;#security-log&lt;/code&gt;)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sS&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$WEBHOOK_URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; @articles/assets/n8n-security-alert-router-slack/samples/low-severity.json
&lt;/code&gt;&lt;/pre&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%2Ftawgqjc01tav4frz9xhl.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%2Ftawgqjc01tav4frz9xhl.png" alt="slack-urgent-alert-received" width="800" height="389"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn0ypue12tlts9l3ge7t7.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%2Fn0ypue12tlts9l3ge7t7.png" alt="slack-log-alert-received" width="800" height="389"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F70c6vitq2azp1vil5uyl.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%2F70c6vitq2azp1vil5uyl.png" alt="n8n-low-execution-success" width="800" height="388"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foun0qcz1gut5z43jtwbh.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%2Foun0qcz1gut5z43jtwbh.png" alt="n8n-high-execution-success" width="800" height="386"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5 — I added a shared secret (before calling it production-ready)
&lt;/h2&gt;

&lt;p&gt;I did not want a public webhook sitting open. I used Webhook &lt;strong&gt;Header Auth&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Webhook → &lt;strong&gt;Options&lt;/strong&gt; → &lt;strong&gt;Authentication&lt;/strong&gt; → &lt;strong&gt;Header Auth&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Header name: &lt;code&gt;X-Security-Webhook-Token&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Value: generated with &lt;code&gt;openssl rand -hex 32&lt;/code&gt;
&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%2Fxymgp9v8evuv8kf1xepm.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%2Fxymgp9v8evuv8kf1xepm.png" alt="Webhook-Token" width="800" height="388"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;CI sender pattern I plan to use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sS&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$WEBHOOK_URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-Security-Webhook-Token: YOUR_TOKEN_HERE"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; @finding.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Verification:&lt;/strong&gt; A request without the header returns &lt;code&gt;401&lt;/code&gt; or a workflow error; a request with the header succeeds.&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%2F2kr6q9e2wbb0dl9i0nnu.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%2F2kr6q9e2wbb0dl9i0nnu.png" alt="request" width="797" height="112"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6 — What I plan to connect next
&lt;/h2&gt;

&lt;p&gt;Once routing was proven, I sketched how to replace the &lt;code&gt;curl&lt;/code&gt; test with a real sender.&lt;/p&gt;

&lt;h3&gt;
  
  
  GitHub Dependabot / code scanning
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Repo → &lt;strong&gt;Settings&lt;/strong&gt; → &lt;strong&gt;Webhooks&lt;/strong&gt; → &lt;strong&gt;Add webhook&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Payload URL: n8n production webhook URL (tunnel like ngrok for local dev)&lt;/li&gt;
&lt;li&gt;Content type: &lt;code&gt;application/json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Events: &lt;strong&gt;Dependabot alerts&lt;/strong&gt; or &lt;strong&gt;Code scanning alerts&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Add a &lt;strong&gt;Code&lt;/strong&gt; or &lt;strong&gt;Set&lt;/strong&gt; node to map GitHub's payload into my six-field contract&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;GitHub's raw payload differs from my contract; the mapping step is intentional and is what makes the router reusable.&lt;/p&gt;

&lt;h3&gt;
  
  
  GitHub Actions after Gitleaks
&lt;/h3&gt;

&lt;p&gt;At the end of a scan job, POST only on failure:&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;Notify security router&lt;/span&gt;
  &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;failure()&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;curl -sS -X POST "${{ secrets.N8N_SECURITY_WEBHOOK }}" \&lt;/span&gt;
      &lt;span class="s"&gt;-H "Content-Type: application/json" \&lt;/span&gt;
      &lt;span class="s"&gt;-H "X-Security-Webhook-Token: ${{ secrets.N8N_WEBHOOK_TOKEN }}" \&lt;/span&gt;
      &lt;span class="s"&gt;-d '{&lt;/span&gt;
        &lt;span class="s"&gt;"source": "gitleaks",&lt;/span&gt;
        &lt;span class="s"&gt;"repo": "${{ github.repository }}",&lt;/span&gt;
        &lt;span class="s"&gt;"severity": "HIGH",&lt;/span&gt;
        &lt;span class="s"&gt;"title": "Gitleaks findings in CI",&lt;/span&gt;
        &lt;span class="s"&gt;"detail": "Workflow ${{ github.workflow }} run ${{ github.run_id }}",&lt;/span&gt;
        &lt;span class="s"&gt;"url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"&lt;/span&gt;
      &lt;span class="s"&gt;}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Webhook URL and token stay in GitHub Actions secrets, never in the workflow file.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I verified it worked
&lt;/h2&gt;

&lt;p&gt;My done checklist:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;HIGH test&lt;/strong&gt; posted to &lt;code&gt;#security-alerts&lt;/code&gt; within seconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LOW test&lt;/strong&gt; posted only to &lt;code&gt;#security-log&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;n8n &lt;strong&gt;Executions&lt;/strong&gt; showed green runs with the correct branch taken.&lt;/li&gt;
&lt;li&gt;Webhook returned &lt;code&gt;200&lt;/code&gt; JSON with &lt;code&gt;status: accepted&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Unauthorized requests (no token) were rejected after I enabled header auth.&lt;/li&gt;
&lt;li&gt;I could explain the flow from the canvas screenshot alone.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  When this breaks down
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Alert storms:&lt;/strong&gt; One repo with hundreds of findings can flood Slack. I would add deduplication (n8n &lt;strong&gt;Remove Duplicates&lt;/strong&gt; node) or aggregate daily digests before urgent routing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payload drift:&lt;/strong&gt; Each tool sends different JSON. Without normalization, the IF node silently misroutes. I would keep one mapping node per source or enforce the standard contract at the sender.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local webhooks:&lt;/strong&gt; GitHub cannot reach &lt;code&gt;localhost&lt;/code&gt;. I would use n8n Cloud, a VPS, or a tunnel for real integrations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slack as SOAR:&lt;/strong&gt; This notifies humans; it does not replace ticketing, on-call paging, or automated remediation. My follow-up would pair this with PagerDuty or Jira.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secrets in workflows:&lt;/strong&gt; I export workflows without credentials; tokens live in n8n Credentials or a secret manager.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: n8n Cloud or self-hosted?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I used self-hosted Docker for learning. n8n Cloud makes sense when I need managed webhooks without exposing a laptop via a tunnel.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Can I use Microsoft Teams instead of Slack?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. Swap the Slack nodes for the Microsoft Teams node. The normalization and IF logic stay the same.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Do I need coding experience?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not for this workflow. Expressions are one-liners. JSON comfort and patience reading execution logs matter more than writing code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Is this "real" DevSecOps?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes, this is &lt;strong&gt;detection-to-notification plumbing&lt;/strong&gt;, a standard first layer in security automation maturity. The mature version adds deduplication, ownership mapping, and ticket creation.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned building this
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Normalize first, route second.&lt;/strong&gt; I almost fought different payload shapes inside the IF node, that is how beginners quit n8n.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two Slack channels beat one.&lt;/strong&gt; Separating urgent from log traffic was the cheapest alert-tuning win I found.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;curl is the best first integration.&lt;/strong&gt; I proved the workflow before touching GitHub webhooks or scanner plugins.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A 20-minute intro video was enough to start.&lt;/strong&gt; The rest was one small project, two test payloads, and reading execution logs when something failed.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.n8n.io/" rel="noopener noreferrer"&gt;n8n documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.webhook/" rel="noopener noreferrer"&gt;n8n Webhook node&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.slack/" rel="noopener noreferrer"&gt;n8n Slack node&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://api.slack.com/start/quickstart" rel="noopener noreferrer"&gt;Slack API — creating an app&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.github.com/en/webhooks/webhook-events-and-payloads" rel="noopener noreferrer"&gt;GitHub webhook events&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://owasp.org/www-project-devsecops-guideline/" rel="noopener noreferrer"&gt;OWASP DevSecOps Guideline&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devsecops</category>
      <category>security</category>
      <category>n8n</category>
      <category>automation</category>
    </item>
    <item>
      <title>How to Design IAM Roles for Multi-Account AWS Environments</title>
      <dc:creator>Ilyas Rufai</dc:creator>
      <pubDate>Wed, 24 Jun 2026 04:09:26 +0000</pubDate>
      <link>https://dev.to/rufilboss/iam-role-design-patterns-for-multi-account-aws-environments-3eng</link>
      <guid>https://dev.to/rufilboss/iam-role-design-patterns-for-multi-account-aws-environments-3eng</guid>
      <description>&lt;p&gt;Your team starts with one AWS account. Six months later, you have five: production, staging, shared services, security, and a sandbox nobody remembers creating. Someone creates an &lt;code&gt;Admin&lt;/code&gt; IAM user in each account. CI/CD stores access keys in three different secret stores. An auditor asks who assumed full access in production last Tuesday, and the answer is a spreadsheet.&lt;/p&gt;

&lt;p&gt;Multi-account AWS is a security win, but only if &lt;strong&gt;access is role-based, scoped, and observable&lt;/strong&gt;. Copying single-account IAM habits into an organization creates credential sprawl, unclear blast radius, and reviews that fail the moment someone says "shared admin user."&lt;/p&gt;

&lt;p&gt;In this article, you will learn five IAM role design patterns that scale across AWS Organizations, when to use each one, and how to implement them with trust policies, guardrails, and Terraform.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who this is for:&lt;/strong&gt; Platform engineers, DevSecOps engineers, and cloud architects building or maturing multi-account AWS environments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you will learn:&lt;/strong&gt; How to design roles for humans, automation, audit, cross-account resources, and third-party access, without defaulting to long-lived keys or duplicated admin policies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Basic familiarity with IAM roles and policies&lt;/li&gt;
&lt;li&gt;An AWS Organization (or plans to adopt one)&lt;/li&gt;
&lt;li&gt;Optional: Terraform for the implementation snippets&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Related work:&lt;/strong&gt; If you are new to role-based EC2 access in a single account, start with &lt;a href="https://dev.to/rufilboss/terraform-ec2-and-s3-upload-access-a-simple-iam-role-setup-without-access-keys-4k99"&gt;Terraform EC2 and S3 Upload Access&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;strong&gt;IAM Identity Center&lt;/strong&gt; (successor to AWS SSO) for human access across accounts, not IAM users in every account.&lt;/li&gt;
&lt;li&gt;Give CI/CD a &lt;strong&gt;dedicated role in a tooling account&lt;/strong&gt; that assumes &lt;strong&gt;short-lived deployment roles&lt;/strong&gt; in workload accounts.&lt;/li&gt;
&lt;li&gt;Centralize audit with &lt;strong&gt;read-only cross-account roles&lt;/strong&gt; into a security/logging account wired to CloudTrail and AWS Config.&lt;/li&gt;
&lt;li&gt;For shared resources (S3, KMS), combine &lt;strong&gt;resource policies&lt;/strong&gt; with &lt;strong&gt;role trust&lt;/strong&gt; and do not make roles overly broad to "make it work."&lt;/li&gt;
&lt;li&gt;Lock the org with &lt;strong&gt;Service Control Policies (SCPs)&lt;/strong&gt;; use &lt;strong&gt;permission boundaries&lt;/strong&gt; on custom roles so teams cannot escalate past intent.&lt;/li&gt;
&lt;li&gt;Every cross-account trust policy should name a &lt;strong&gt;specific principal&lt;/strong&gt;, add &lt;strong&gt;conditions&lt;/strong&gt; (&lt;code&gt;aws:PrincipalOrgID&lt;/code&gt;, &lt;code&gt;aws:PrincipalAccount&lt;/code&gt;), and be visible in CloudTrail.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why single-account IAM thinking fails at scale
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Habit from one account&lt;/th&gt;
&lt;th&gt;What breaks in multi-account&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;IAM users with access keys per engineer&lt;/td&gt;
&lt;td&gt;Keys multiply; rotation and offboarding become error-prone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One &lt;code&gt;AdministratorAccess&lt;/code&gt; role cloned everywhere&lt;/td&gt;
&lt;td&gt;No separation between prod and sandbox; blast radius is the whole org&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI/CD keys stored per environment&lt;/td&gt;
&lt;td&gt;Leaked key in staging can affect production if policies are loose&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"We'll clean it up later" bucket policies&lt;/td&gt;
&lt;td&gt;Cross-account access becomes permanent &lt;code&gt;Principal: "*"&lt;/code&gt; debt&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; In multi-account AWS, identity lives in one place, authorization is &lt;strong&gt;scoped per account and per job function&lt;/strong&gt;, and guardrails apply &lt;strong&gt;above&lt;/strong&gt; IAM with SCPs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reference account model
&lt;/h2&gt;

&lt;p&gt;You do not need a perfect landing zone on day one, but roles make more sense when accounts have intent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Organization
├── Security OU
│   └── security-audit (log archive, read-only aggregation)
├── Infrastructure OU
│   └── shared-networking (optional)
├── Workloads OU
│   ├── prod
│   ├── staging
│   └── dev
└── Sandbox OU
    └── sandbox
&lt;/code&gt;&lt;/pre&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%2Fkfy361cpw03qpz9g9zzg.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%2Fkfy361cpw03qpz9g9zzg.png" alt="AWS Organizations OU hierarchy with security, workload, and sandbox accounts" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6fz4c5e7vzsps8iwi0l3.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%2F6fz4c5e7vzsps8iwi0l3.png" alt="Multi-account IAM role patterns across security, tooling, and workload accounts" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each pattern below maps to a &lt;strong&gt;different trust direction&lt;/strong&gt;. Mixing them into one "super role" is the most common design mistake.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 1 — Human access with IAM Identity Center
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Use when:&lt;/strong&gt; Engineers, operators, or auditors need console or CLI access to multiple accounts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not:&lt;/strong&gt; Create IAM users in every account and email access keys.&lt;/p&gt;

&lt;p&gt;IAM Identity Center issues short-lived credentials via permission sets mapped to groups (for example, &lt;code&gt;PlatformAdmins&lt;/code&gt;, &lt;code&gt;Developers&lt;/code&gt;, &lt;code&gt;ReadOnlyAuditors&lt;/code&gt;). AWS automatically creates corresponding roles in member accounts.&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%2F2iavgq2tnw0gpy8csq1z.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%2F2iavgq2tnw0gpy8csq1z.png" alt="IAM Identity Center human access flow across permission sets and member accounts" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Design rules:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Map job function to permission sets, not individuals to admin.&lt;/li&gt;
&lt;li&gt;Prefer &lt;code&gt;ReadOnlyAccess&lt;/code&gt; or custom read sets for most engineers; reserve power access for break-glass workflows.&lt;/li&gt;
&lt;li&gt;Use account assignments so &lt;code&gt;Developers&lt;/code&gt; get &lt;code&gt;PowerUserAccess&lt;/code&gt; in &lt;code&gt;dev&lt;/code&gt; but &lt;code&gt;ReadOnlyAccess&lt;/code&gt; in &lt;code&gt;prod&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Enable IAM Identity Center in the &lt;strong&gt;management account&lt;/strong&gt; (or a dedicated identity account if your org design uses one).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Verification signal:&lt;/strong&gt; A user in the &lt;code&gt;Developers&lt;/code&gt; group can open the &lt;code&gt;dev&lt;/code&gt; account console but receives &lt;code&gt;AccessDenied&lt;/code&gt; when attempting the same action in &lt;code&gt;prod&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;IAM Identity Center → Permission sets; predefined&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%2Fzg4vlwn4gugcbr785075.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%2Fzg4vlwn4gugcbr785075.png" alt="IAM Identity Center Predefined1" width="800" height="376"&gt;&lt;/a&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%2F8bvemldgtnpy2pb21oro.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%2F8bvemldgtnpy2pb21oro.png" alt="IAM Identity Center Predefined2" width="800" height="376"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;IAM Identity Center → Permission sets; custom&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%2Fp6ow7ogpy5eo0kp0bl1v.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%2Fp6ow7ogpy5eo0kp0bl1v.png" alt="IAM Identity Center Custom" width="800" height="376"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Pattern 2 — CI/CD deployment role (tooling → workload)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Use when:&lt;/strong&gt; GitHub Actions, GitLab CI, Jenkins, or CodePipeline deploys infrastructure or applications into workload accounts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trust flow:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Tooling account (CI role)
        |
        | sts:AssumeRole
        v
Workload account (deployment role)
        |
        v
Deploy EC2 / ECS / Lambda / Terraform resources
&lt;/code&gt;&lt;/pre&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%2F11ox6srmv0neaobyoxug.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%2F11ox6srmv0neaobyoxug.png" alt="CI/CD deployment role trust flow from tooling account to workload account" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Trust policy in the workload account (deployment role)
&lt;/h3&gt;

&lt;p&gt;This role lives in &lt;strong&gt;production&lt;/strong&gt; (or staging) and trusts a &lt;strong&gt;specific role&lt;/strong&gt; in the tooling account, not the whole organization.&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;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AllowToolingDeployRole"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Principal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"AWS"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:iam::111111111111:role/github-actions-deploy"&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:AssumeRole"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"StringEquals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"aws:PrincipalOrgID"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"o-a1b2c3d4e5"&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;
  
  
  Permissions policy on the deployment role (example)
&lt;/h3&gt;

&lt;p&gt;Scope to what the pipeline actually does, not &lt;code&gt;AdministratorAccess&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"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;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DeployApplicationStack"&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;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"ec2:Describe*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"ecs:UpdateService"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"ecs:DescribeServices"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"iam:PassRole"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"StringEquals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"iam:PassedToService"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ecs-tasks.amazonaws.com"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Terraform: workload account role
&lt;/h3&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;"tooling_account_id"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"organization_id"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"tooling_deploy_role_name"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"github-actions-deploy"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role"&lt;/span&gt; &lt;span class="s2"&gt;"workload_deploy"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"deployment-role"&lt;/span&gt;

  &lt;span class="nx"&gt;assume_role_policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonencode&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;Version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;
    &lt;span class="nx"&gt;Statement&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
      &lt;span class="nx"&gt;Sid&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AllowToolingDeployRole"&lt;/span&gt;
      &lt;span class="nx"&gt;Effect&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
      &lt;span class="nx"&gt;Principal&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;AWS&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"arn:aws:iam::${var.tooling_account_id}:role/${var.tooling_deploy_role_name}"&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nx"&gt;Action&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"sts:AssumeRole"&lt;/span&gt;
      &lt;span class="nx"&gt;Condition&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;StringEquals&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:PrincipalOrgID"&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;organization_id&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;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Design rules:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;One deployment role per environment (&lt;code&gt;deployment-role-prod&lt;/code&gt;, &lt;code&gt;deployment-role-staging&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;CI authenticates to the tooling account first (prefer &lt;strong&gt;OIDC federation&lt;/strong&gt;, not stored keys).&lt;/li&gt;
&lt;li&gt;Pipeline assumes the workload role immediately before deploy steps.&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;aws:PrincipalAccount&lt;/code&gt; if multiple accounts exist in the org and you want extra-tight trust.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Verification signal:&lt;/strong&gt; CloudTrail shows &lt;code&gt;AssumeRole&lt;/code&gt; from the tooling role ARN into the workload deployment role, followed only by expected API calls, no stray &lt;code&gt;iam:CreateUser&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 3 — Central audit and security read role
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Use when:&lt;/strong&gt; A security account aggregates CloudTrail, AWS Config, GuardDuty, or Security Hub findings from member accounts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trust flow:&lt;/strong&gt; Member accounts expose a &lt;strong&gt;read-only role&lt;/strong&gt; that the security account (or a security automation role) can assume, or use organization-wide services (Security Hub, GuardDuty delegated admin) where applicable.&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%2Fjfzllesomm2xzpufgt7e.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%2Fjfzllesomm2xzpufgt7e.png" alt="Central audit read role flow from member accounts to security account" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Example member-account role trust:&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;"AWS"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:iam::999999999999:role/security-read-automation"&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:AssumeRole"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"StringEquals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"aws:PrincipalOrgID"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"o-a1b2c3d4e5"&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;Attach a read-only policy (&lt;code&gt;SecurityAudit&lt;/code&gt; or tighter custom read).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Design rules:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Security account is for &lt;strong&gt;read and detect&lt;/strong&gt;, not daily deploys.&lt;/li&gt;
&lt;li&gt;Prefer organization-level service delegation (GuardDuty, Security Hub) where available instead of hundreds of custom trust relationships.&lt;/li&gt;
&lt;li&gt;Log every cross-account assumption; alert on unexpected principals.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Verification signal:&lt;/strong&gt; A security account can describe Config rules in member accounts but cannot &lt;code&gt;ec2:TerminateInstances&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 4 — Cross-account resource access (S3 and KMS)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Use when:&lt;/strong&gt; A workload in account A must read/write a bucket or key in account B (logs, artifacts, shared datasets).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not:&lt;/strong&gt; Give the compute role in account A &lt;code&gt;s3:*&lt;/code&gt; on &lt;code&gt;*&lt;/code&gt; and call it done.&lt;/p&gt;

&lt;p&gt;You need &lt;strong&gt;both sides&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Identity policy&lt;/strong&gt; on the role in account A (what the role is allowed to request).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resource policy&lt;/strong&gt; on the bucket or KMS key in account B (what the resource allows).&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%2Fw0gnfwlu4iae7gxz9ctt.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%2Fw0gnfwlu4iae7gxz9ctt.png" alt="Cross-account S3 access requiring identity policy and resource policy on both sides" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  S3 bucket policy (account B)
&lt;/h3&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;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AllowWorkloadRoleUpload"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Principal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"AWS"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:iam::222222222222:role/app-upload-role"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"s3:PutObject"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"s3:AbortMultipartUpload"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3:::shared-artifacts-prod/uploads/*"&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;
  
  
  Identity policy (account A, on &lt;code&gt;app-upload-role&lt;/code&gt;)
&lt;/h3&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;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"WriteSharedArtifacts"&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;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"s3:PutObject"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"s3:AbortMultipartUpload"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3:::shared-artifacts-prod/uploads/*"&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;For KMS, include &lt;code&gt;kms:Encrypt&lt;/code&gt;, &lt;code&gt;kms:GenerateDataKey&lt;/code&gt;, and a key policy that names the same principal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Design rules:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Name the &lt;strong&gt;exact role ARN&lt;/strong&gt; in the resource policy; avoid account-root principals unless you truly mean the entire account.&lt;/li&gt;
&lt;li&gt;Scope to prefix (&lt;code&gt;uploads/*&lt;/code&gt;) or specific key aliases.&lt;/li&gt;
&lt;li&gt;Prefer moving the resource closer to the consumer account if daily cross-account access is heavy.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Verification signal:&lt;/strong&gt; Upload succeeds from the app role; &lt;code&gt;s3:DeleteObject&lt;/code&gt; or access from a different role fails with &lt;code&gt;AccessDenied&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 5 — Third-party and vendor access with ExternalId
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Use when:&lt;/strong&gt; A SaaS vendor, partner, or integration must assume a role in your account (monitoring, backup, security scanning).&lt;/p&gt;

&lt;p&gt;Always require &lt;code&gt;sts:ExternalId&lt;/code&gt; in the trust policy to mitigate confused deputy problems.&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%2F12x1k919aa3u5jmrysv7.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%2F12x1k919aa3u5jmrysv7.png" alt="Third-party vendor assume role flow with ExternalId condition" width="800" height="533"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"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;"AWS"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:iam::333333333333:root"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sts:AssumeRole"&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;"sts:ExternalId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"your-unique-external-id-from-vendor"&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;&lt;strong&gt;Design rules:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;One role per vendor integration; never reuse a vendor role for internal automation.&lt;/li&gt;
&lt;li&gt;Rotate or revoke ExternalId when offboarding the vendor.&lt;/li&gt;
&lt;li&gt;Prefer vendor-specific managed policies over &lt;code&gt;ReadOnlyAccess&lt;/code&gt; if the vendor documents minimum requirements.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  SCPs and permission boundaries: guardrails above IAM
&lt;/h2&gt;

&lt;p&gt;IAM roles express &lt;strong&gt;intent&lt;/strong&gt;. SCPs express &lt;strong&gt;organizational maximums&lt;/strong&gt;. Permission boundaries cap what a role can ultimately grant, even if someone attaches a broader policy (by mistake).&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%2Fy76t7ubqfkvw2adhmf24.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%2Fy76t7ubqfkvw2adhmf24.png" alt="IAM guardrails layered with allow policies, permission boundaries, and SCP deny ceiling" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Control&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SCP&lt;/td&gt;
&lt;td&gt;Org-wide deny ceiling&lt;/td&gt;
&lt;td&gt;Deny all actions outside &lt;code&gt;af-south-1&lt;/code&gt; and &lt;code&gt;eu-west-1&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Permission boundary&lt;/td&gt;
&lt;td&gt;Max permissions for a custom role&lt;/td&gt;
&lt;td&gt;Allow EC2 and ECS, deny IAM and Organizations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Session policy&lt;/td&gt;
&lt;td&gt;One-time session scoping&lt;/td&gt;
&lt;td&gt;CLI assume-role with a tighter inline session policy&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A common SCP pattern for workload accounts:&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;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DenyIAMUserAndAccessKeys"&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;"Deny"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"iam:CreateUser"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"iam:CreateAccessKey"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="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;SCPs do not grant access; instead, they filter it. IAM roles still need explicit allow policies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision matrix: which pattern when?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Need&lt;/th&gt;
&lt;th&gt;Pattern&lt;/th&gt;
&lt;th&gt;Primary mechanism&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Engineer console/CLI access&lt;/td&gt;
&lt;td&gt;Identity Center&lt;/td&gt;
&lt;td&gt;Permission sets + account assignments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pipeline deploys to prod&lt;/td&gt;
&lt;td&gt;CI/CD deployment role&lt;/td&gt;
&lt;td&gt;Cross-account &lt;code&gt;AssumeRole&lt;/code&gt; + OIDC in tooling account&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security team visibility&lt;/td&gt;
&lt;td&gt;Audit read role&lt;/td&gt;
&lt;td&gt;Cross-account read + delegated security services&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;App writes to shared bucket&lt;/td&gt;
&lt;td&gt;Resource + identity policy&lt;/td&gt;
&lt;td&gt;S3/KMS policies naming specific role ARNs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vendor integration&lt;/td&gt;
&lt;td&gt;Vendor assume-role&lt;/td&gt;
&lt;td&gt;Trust policy + &lt;code&gt;sts:ExternalId&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prevent IAM users org-wide&lt;/td&gt;
&lt;td&gt;SCP guardrail&lt;/td&gt;
&lt;td&gt;Deny &lt;code&gt;iam:CreateUser&lt;/code&gt; / &lt;code&gt;CreateAccessKey&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  How to verify your design
&lt;/h2&gt;

&lt;p&gt;Run this review before calling the model "done":&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;No long-lived IAM user keys&lt;/strong&gt; in workload or tooling accounts for automation.&lt;/li&gt;
&lt;li&gt;Every cross-account trust policy names a &lt;strong&gt;specific principal ARN&lt;/strong&gt; (or Identity Center pattern), not &lt;code&gt;*&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;aws:PrincipalOrgID&lt;/code&gt; (or &lt;code&gt;aws:PrincipalAccount&lt;/code&gt;) appears on cross-account trust where possible.&lt;/li&gt;
&lt;li&gt;CloudTrail logs show &lt;strong&gt;who assumed which role&lt;/strong&gt; and which API calls followed.&lt;/li&gt;
&lt;li&gt;A negative test fails as expected (for example, staging deploy role cannot assume production role).&lt;/li&gt;
&lt;li&gt;SCP deny rules block the risk you fear most (root usage, unapproved regions, public S3 ACLs).&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Quick CLI negative test (deployment roles)
&lt;/h3&gt;

&lt;p&gt;From the tooling role session, attempt to assume a role that you should not reach:&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 assume-role &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--role-arn&lt;/span&gt; arn:aws:iam::PROD_ACCOUNT_ID:role/deployment-role &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--role-session-name&lt;/span&gt; deploy-test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Repeat with the &lt;strong&gt;allowed&lt;/strong&gt; workload role and confirm success. Then run a single deploy action and confirm that CloudTrail captures the chain.&lt;/p&gt;

&lt;h2&gt;
  
  
  When this breaks down
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Account vending without role standards:&lt;/strong&gt; New accounts get bespoke admin users because there is no template for trust policies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Role chaining depth:&lt;/strong&gt; Chaining multiple &lt;code&gt;AssumeRole&lt;/code&gt; calls hits session limits and makes debugging painful; keep chains shallow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Overloaded deployment roles:&lt;/strong&gt; One role deploys EC2, edits IAM, and reads Secrets Manager; incident response becomes guesswork.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SCP vs IAM confusion:&lt;/strong&gt; Teams think an IAM &lt;code&gt;Allow&lt;/code&gt; fixes an SCP &lt;code&gt;Deny&lt;/code&gt;; it does not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing break-glass:&lt;/strong&gt; Identity Center power access with no emergency process leads to shared root usage in outages.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: Should I use IAM users at all in member accounts?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For human access, no, use IAM Identity Center. Limited exceptions exist for break-glass automation accounts with strict controls, but that is not the default pattern.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Is &lt;code&gt;AdministratorAccess&lt;/code&gt; ever okay for deployment roles?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Rarely in mature environments. Start tight; broaden only with recorded justification. Admin deploy roles turn pipeline compromise into organization compromise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Do I need a separate tooling account?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not on day one, but CI/CD roles benefit from isolation. Many teams start with a &lt;code&gt;shared-services&lt;/code&gt; account and split later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How does this relate to AWS Control Tower?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Control Tower encodes opinionated OU and account factory patterns. The role designs here still apply; Control Tower helps enforce account baseline and guardrails.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned designing roles across accounts
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Trust policies are the real contract&lt;/strong&gt; between accounts; review them like production code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conditions are cheap insurance&lt;/strong&gt;; &lt;code&gt;aws:PrincipalOrgID&lt;/code&gt; stops a surprising amount of cross-account trust mistakes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Readable role names&lt;/strong&gt; (&lt;code&gt;deployment-role-prod&lt;/code&gt;, &lt;code&gt;security-read-member&lt;/code&gt;) beat generic &lt;code&gt;cross-account-role&lt;/code&gt; when auditors and on-call engineers ask questions at 2 a.m.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/singlesignon/latest/userguide/what-is.html" rel="noopener noreferrer"&gt;IAM Identity Center&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles.html" rel="noopener noreferrer"&gt;IAM roles&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies-cross-account-resource-access.html" rel="noopener noreferrer"&gt;Cross-account resource access in IAM&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/organizations/latest/userguide/orgs_manage_policies_scps.html" rel="noopener noreferrer"&gt;AWS Organizations SCPs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_boundaries.html" rel="noopener noreferrer"&gt;IAM permission boundaries&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/confused-deputy.html" rel="noopener noreferrer"&gt;Confused deputy problem (ExternalId)&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;Security best practices in IAM&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/wellarchitected/latest/security-pillar/welcome.html" rel="noopener noreferrer"&gt;AWS Well-Architected Framework — Security pillar&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>iam</category>
      <category>aws</category>
      <category>security</category>
      <category>infrastructure</category>
    </item>
    <item>
      <title>Terraform EC2 and S3 Upload Access: A Simple IAM Role Setup Without Access Keys</title>
      <dc:creator>Ilyas Rufai</dc:creator>
      <pubDate>Sun, 21 Jun 2026 07:05:28 +0000</pubDate>
      <link>https://dev.to/rufilboss/terraform-ec2-and-s3-upload-access-a-simple-iam-role-setup-without-access-keys-4k99</link>
      <guid>https://dev.to/rufilboss/terraform-ec2-and-s3-upload-access-a-simple-iam-role-setup-without-access-keys-4k99</guid>
      <description>&lt;p&gt;You need a small virtual machine that can upload files to object storage. The fast-but-wrong path is to create an Identity and Access Management (IAM) access key, SSH into the box, and paste credentials into &lt;code&gt;~/.aws/credentials&lt;/code&gt;. That works once, then those keys live on disk, get copied into scripts, and eventually leak.&lt;/p&gt;

&lt;p&gt;In this article, you will use Terraform to launch an Elastic Compute Cloud (EC2) instance and attach an IAM role so the instance can upload to one Simple Storage Service (S3) bucket without storing AWS access keys on the server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who this is for:&lt;/strong&gt; Developers and DevOps engineers learning Terraform on AWS.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you will build:&lt;/strong&gt; One EC2 instance, one private S3 bucket, one IAM role with least-privilege upload permissions, and a working &lt;code&gt;aws s3 cp&lt;/code&gt; test from the instance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An AWS account with permissions to create EC2, S3, IAM, and security groups&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.hashicorp.com/terraform/install" rel="noopener noreferrer"&gt;Terraform&lt;/a&gt; 1.5+ installed locally&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html" rel="noopener noreferrer"&gt;AWS CLI&lt;/a&gt; configured locally (&lt;code&gt;aws configure&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;An EC2 key pair already created in your target AWS Region (for SSH access)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Versions used in this tutorial:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Terraform &lt;code&gt;1.5+&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;AWS provider &lt;code&gt;~&amp;gt; 5.0&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Amazon Linux 2023 AMI&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Do not put IAM access keys on EC2 for S3 uploads; use an IAM instance profile instead.&lt;/li&gt;
&lt;li&gt;Create the S3 bucket first, then attach a role policy scoped to that bucket and an &lt;code&gt;uploads/&lt;/code&gt; prefix.&lt;/li&gt;
&lt;li&gt;Launch EC2 in your default virtual private cloud (VPC) with a security group that allows SSH only from your IP.&lt;/li&gt;
&lt;li&gt;Verify access from the instance with &lt;code&gt;aws s3 cp&lt;/code&gt; credentials that come from instance metadata automatically.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;terraform destroy&lt;/code&gt; when finished to avoid ongoing EC2 charges.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why putting access keys on EC2 fails
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Why teams use it&lt;/th&gt;
&lt;th&gt;Why it breaks&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;IAM user access keys on the server&lt;/td&gt;
&lt;td&gt;Fast to test uploads&lt;/td&gt;
&lt;td&gt;Keys sit on disk, are hard to rotate, and expand blast radius if the instance is compromised&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Overly broad IAM policy (&lt;code&gt;s3:*&lt;/code&gt; on &lt;code&gt;*&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Fewer permission errors during setup&lt;/td&gt;
&lt;td&gt;One compromised instance can read, delete, or overwrite unrelated buckets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manual console clicks only&lt;/td&gt;
&lt;td&gt;No infrastructure as code (IaC) history&lt;/td&gt;
&lt;td&gt;Drift, no repeatable setup, hard to audit&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; The EC2 instance should assume an IAM role at boot. AWS injects short-lived credentials through the instance metadata service. Your Terraform code documents exactly what the instance can do.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you are building
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Your laptop (Terraform)
        |
        v
   AWS API calls
        |
        +--&amp;gt; S3 bucket (uploads/ prefix)
        |
        +--&amp;gt; IAM role + instance profile
        |
        +--&amp;gt; EC2 instance (assumes role, uploads via AWS CLI)
&lt;/code&gt;&lt;/pre&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%2F838etnc5xvlg68uh9thy.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%2F838etnc5xvlg68uh9thy.png" alt="Architecture flow: Terraform provisions EC2 with IAM role for S3 uploads" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Create the project layout
&lt;/h2&gt;

&lt;p&gt;Create a new folder and four files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;terraform-ec2-s3-upload &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;terraform-ec2-s3-upload
&lt;span class="nb"&gt;touch &lt;/span&gt;versions.tf variables.tf main.tf outputs.tf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your layout should look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;terraform-ec2-s3-upload/
  versions.tf
  variables.tf
  main.tf
  outputs.tf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: Pin provider versions
&lt;/h2&gt;

&lt;p&gt;Add provider constraints in &lt;code&gt;versions.tf&lt;/code&gt;:&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;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;required_version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&amp;gt;= 1.5.0"&lt;/span&gt;

  &lt;span class="nx"&gt;required_providers&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;aws&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;source&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"hashicorp/aws"&lt;/span&gt;
      &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"~&amp;gt; 5.0"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="s2"&gt;"aws"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;region&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;aws_region&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3: Define input variables
&lt;/h2&gt;

&lt;p&gt;Add tunable values in &lt;code&gt;variables.tf&lt;/code&gt;:&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;"aws_region"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AWS region to deploy resources"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"us-east-1"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"project_name"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Prefix for resource names"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"tf-ec2-s3-demo"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"instance_type"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"EC2 instance type"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"t3.micro"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"key_name"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Existing EC2 key pair name in this region"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"ssh_cidr"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Your public IP in CIDR format for SSH access (example: 203.0.113.10/32)"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a &lt;code&gt;terraform.tfvars&lt;/code&gt; file with your values:&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;aws_region&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"us-east-1"&lt;/span&gt;
&lt;span class="nx"&gt;key_name&lt;/span&gt;   &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"your-key-pair-name"&lt;/span&gt;
&lt;span class="nx"&gt;ssh_cidr&lt;/span&gt;   &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"YOUR_PUBLIC_IP/32"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To find your public IP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://checkip.amazonaws.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Append &lt;code&gt;/32&lt;/code&gt; to that IP for &lt;code&gt;ssh_cidr&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe85ld0yj6n62k77vqs7s.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%2Fe85ld0yj6n62k77vqs7s.png" alt="public_ip" width="798" height="54"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Create the S3 bucket
&lt;/h2&gt;

&lt;p&gt;Add the bucket and hardening settings in &lt;code&gt;main.tf&lt;/code&gt;:&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_s3_bucket"&lt;/span&gt; &lt;span class="s2"&gt;"uploads"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.project_name}-${data.aws_caller_identity.current.account_id}"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket_public_access_block"&lt;/span&gt; &lt;span class="s2"&gt;"uploads"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_s3_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uploads&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;block_public_acls&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;block_public_policy&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;ignore_public_acls&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;restrict_public_buckets&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_caller_identity"&lt;/span&gt; &lt;span class="s2"&gt;"current"&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bucket name includes your account ID to reduce naming collisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Create IAM role and least-privilege S3 policy
&lt;/h2&gt;

&lt;p&gt;Still in &lt;code&gt;main.tf&lt;/code&gt;, define what the EC2 instance is allowed to do:&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;"ec2_assume_role"&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;actions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"sts:AssumeRole"&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="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Service"&lt;/span&gt;
      &lt;span class="nx"&gt;identifiers&lt;/span&gt; &lt;span class="p"&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="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role"&lt;/span&gt; &lt;span class="s2"&gt;"ec2_upload_role"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;               &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.project_name}-ec2-role"&lt;/span&gt;
  &lt;span class="nx"&gt;assume_role_policy&lt;/span&gt; &lt;span class="p"&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_iam_policy_document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ec2_assume_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&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;"s3_upload_policy"&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;sid&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ListBucketPrefix"&lt;/span&gt;
    &lt;span class="nx"&gt;effect&lt;/span&gt; &lt;span class="p"&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="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="s2"&gt;"s3:ListBucket"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_s3_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uploads&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;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="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"StringLike"&lt;/span&gt;
      &lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"s3:prefix"&lt;/span&gt;
      &lt;span class="nx"&gt;values&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"uploads/*"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;sid&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"UploadObjectsOnly"&lt;/span&gt;
    &lt;span class="nx"&gt;effect&lt;/span&gt; &lt;span class="p"&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="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="s2"&gt;"s3:PutObject"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"s3:AbortMultipartUpload"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;resources&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_s3_bucket.uploads.arn}/uploads/*"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_policy"&lt;/span&gt; &lt;span class="s2"&gt;"s3_upload_policy"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.project_name}-s3-upload"&lt;/span&gt;
  &lt;span class="nx"&gt;policy&lt;/span&gt; &lt;span class="p"&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_iam_policy_document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;s3_upload_policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role_policy_attachment"&lt;/span&gt; &lt;span class="s2"&gt;"attach_upload_policy"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;role&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ec2_upload_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;policy_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;s3_upload_policy&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;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_instance_profile"&lt;/span&gt; &lt;span class="s2"&gt;"ec2_profile"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.project_name}-instance-profile"&lt;/span&gt;
  &lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ec2_upload_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This role can upload only to &lt;code&gt;uploads/*&lt;/code&gt; inside one bucket. It cannot delete objects or read unrelated buckets.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Use default VPC networking and security group
&lt;/h2&gt;

&lt;p&gt;For a beginner-friendly setup, use the default VPC and one public subnet:&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_vpc"&lt;/span&gt; &lt;span class="s2"&gt;"default"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_subnets"&lt;/span&gt; &lt;span class="s2"&gt;"default"&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="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"vpc-id"&lt;/span&gt;
    &lt;span class="nx"&gt;values&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&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_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;default&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="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_security_group"&lt;/span&gt; &lt;span class="s2"&gt;"ec2_sg"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.project_name}-sg"&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow SSH from your IP only"&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt;      &lt;span class="p"&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_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;default&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;ingress&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"SSH from admin IP"&lt;/span&gt;
    &lt;span class="nx"&gt;from_port&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;22&lt;/span&gt;
    &lt;span class="nx"&gt;to_port&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;22&lt;/span&gt;
    &lt;span class="nx"&gt;protocol&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"tcp"&lt;/span&gt;
    &lt;span class="nx"&gt;cidr_blocks&lt;/span&gt; &lt;span class="p"&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;ssh_cidr&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;egress&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;from_port&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="nx"&gt;to_port&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="nx"&gt;protocol&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"-1"&lt;/span&gt;
    &lt;span class="nx"&gt;cidr_blocks&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"0.0.0.0/0"&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;h2&gt;
  
  
  Step 7: Launch the EC2 instance
&lt;/h2&gt;

&lt;p&gt;Add the Amazon Linux 2023 AMI lookup and EC2 instance:&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_2023"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;most_recent&lt;/span&gt; &lt;span class="p"&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="p"&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="p"&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="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"al2023-ami-*-kernel-*-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="p"&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="p"&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;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_instance"&lt;/span&gt; &lt;span class="s2"&gt;"app"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;ami&lt;/span&gt;                    &lt;span class="p"&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_2023&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="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;instance_type&lt;/span&gt;
  &lt;span class="nx"&gt;subnet_id&lt;/span&gt;              &lt;span class="p"&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_subnets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;default&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_security_group_ids&lt;/span&gt; &lt;span class="p"&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;ec2_sg&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;key_name&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;key_name&lt;/span&gt;
  &lt;span class="nx"&gt;iam_instance_profile&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_instance_profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ec2_profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;

  &lt;span class="nx"&gt;user_data&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;-&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
              #!/bin/bash
              dnf update -y
              dnf install -y awscli
&lt;/span&gt;&lt;span class="no"&gt;              EOF

&lt;/span&gt;  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.project_name}-ec2"&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;Add outputs in &lt;code&gt;outputs.tf&lt;/code&gt;:&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;output&lt;/span&gt; &lt;span class="s2"&gt;"ec2_public_ip"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Public IP for SSH"&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;public_ip&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"s3_bucket_name"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Target bucket for uploads"&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_s3_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uploads&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;output&lt;/span&gt; &lt;span class="s2"&gt;"ssh_command"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"SSH command example"&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ssh -i ~/.ssh/${var.key_name}.pem ec2-user@${aws_instance.app.public_ip}"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 8: Initialize and apply Terraform
&lt;/h2&gt;

&lt;p&gt;Run Terraform from the project folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform init
terraform plan
terraform apply
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;terraform init&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%2Ftdj51k9szn2n1q05im1m.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%2Ftdj51k9szn2n1q05im1m.png" alt="Terraform init" width="799" height="256"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;terraform plan&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%2Ffnhrr7brh749ctwxmhil.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%2Ffnhrr7brh749ctwxmhil.png" alt="Terraform Plan" width="800" height="564"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;terraform apply; Type &lt;code&gt;yes&lt;/code&gt; when prompted.&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%2Fayl5ll7kj23zzyooddwk.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%2Fayl5ll7kj23zzyooddwk.png" alt="Terraform apply" width="800" height="575"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F40no3iwfgs9rcidb88zv.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%2F40no3iwfgs9rcidb88zv.png" alt="Terraform Applied" width="800" height="575"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Instance running:&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%2Fpdwgf3uomw76849cyifs.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%2Fpdwgf3uomw76849cyifs.png" alt="Success!" width="797" height="107"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 9: SSH into the instance
&lt;/h2&gt;

&lt;p&gt;Use the output SSH command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh &lt;span class="nt"&gt;-i&lt;/span&gt; ~/.ssh/your-key-pair-name.pem ec2-user@&amp;lt;EC2_PUBLIC_IP&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the connection fails, confirm:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;ssh_cidr&lt;/code&gt; matches your current public IP.&lt;/li&gt;
&lt;li&gt;Your key file permissions are &lt;code&gt;chmod 400&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The instance status is &lt;code&gt;running&lt;/code&gt;.&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%2F9ip6abfq5qp6h0hkdv2q.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%2F9ip6abfq5qp6h0hkdv2q.png" alt="successful SSH session" width="800" height="407"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 10: Verify S3 upload from EC2
&lt;/h2&gt;

&lt;p&gt;On the EC2 instance, confirm AWS CLI identity:&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;You should see the role ARN for &lt;code&gt;${project_name}-ec2-role&lt;/code&gt;, not an IAM user.&lt;/p&gt;

&lt;p&gt;Upload a test file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"upload test &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /tmp/upload-test.txt
aws s3 &lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/upload-test.txt s3://&amp;lt;BUCKET_NAME&amp;gt;/uploads/upload-test.txt
aws s3 &lt;span class="nb"&gt;ls &lt;/span&gt;s3://&amp;lt;BUCKET_NAME&amp;gt;/uploads/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;&amp;lt;BUCKET_NAME&amp;gt;&lt;/code&gt; with the &lt;code&gt;s3_bucket_name&lt;/code&gt; Terraform output.&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%2Fc75lni4ap4iacjjwuyxl.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%2Fc75lni4ap4iacjjwuyxl.png" alt="s3-upload-success" width="800" height="62"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Filhpmr27a3y0pvh6c2zk.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%2Filhpmr27a3y0pvh6c2zk.png" alt="08-s3-console-object" width="799" height="185"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How to verify this works
&lt;/h2&gt;

&lt;p&gt;Run this quick checklist:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;terraform output&lt;/code&gt; shows &lt;code&gt;ec2_public_ip&lt;/code&gt; and &lt;code&gt;s3_bucket_name&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;aws sts get-caller-identity&lt;/code&gt; on EC2 returns the instance role ARN.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;aws s3 cp&lt;/code&gt; to &lt;code&gt;uploads/&lt;/code&gt; succeeds from EC2.&lt;/li&gt;
&lt;li&gt;Upload to a different prefix (for example, &lt;code&gt;secrets/&lt;/code&gt;) fails with &lt;code&gt;AccessDenied&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;No access keys exist in &lt;code&gt;/home/ec2-user/.aws/credentials&lt;/code&gt; on the instance.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The last check confirms you are using role-based access, not static keys.&lt;/p&gt;

&lt;h2&gt;
  
  
  Clean up to avoid charges
&lt;/h2&gt;

&lt;p&gt;When you finish testing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform destroy
&lt;/code&gt;&lt;/pre&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%2Fhf69y2nvf7o6uj4u2i32.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%2Fhf69y2nvf7o6uj4u2i32.png" alt="Destroying resources" width="800" height="577"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Type &lt;code&gt;yes&lt;/code&gt; to remove EC2, IAM, and S3 resources created by this tutorial.&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%2Fks2ehftolssuvh5vkhix.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%2Fks2ehftolssuvh5vkhix.png" alt="Destroy complete!" width="800" height="577"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  When this breaks down
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Default VPC missing:&lt;/strong&gt; Some AWS accounts disable the default VPC. You will need to create a VPC and public subnet first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SSH blocked by IP change:&lt;/strong&gt; If your home IP changes, update &lt;code&gt;ssh_cidr&lt;/code&gt; and run &lt;code&gt;terraform apply&lt;/code&gt; again.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Too open security groups:&lt;/strong&gt; Never set SSH to &lt;code&gt;0.0.0.0/0&lt;/code&gt; in real environments.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Policy too broad:&lt;/strong&gt; &lt;code&gt;s3:*&lt;/code&gt; on &lt;code&gt;*&lt;/code&gt; is convenient for demos but unsafe for production workloads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No remote state backend:&lt;/strong&gt; This tutorial uses local Terraform state. For team use, move state to S3 with locking (DynamoDB).&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: Why not attach an IAM user access key to EC2?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Access keys are long-lived secrets. IAM roles provide temporary credentials that rotate automatically and are tied to the instance lifecycle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Can this instance read objects from S3?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not with the policy in this tutorial. It can upload to &lt;code&gt;uploads/*&lt;/code&gt; only. Add explicit &lt;code&gt;s3:GetObject&lt;/code&gt; permissions only if your workload needs reads.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Is this production-ready?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It is a strong learning baseline, not a production baseline. Production setups usually add private subnets, Systems Manager (SSM) Session Manager instead of public SSH, encryption, logging, and tighter network controls.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned building this
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;IAM instance profiles are the default pattern for EC2-to-AWS service access.&lt;/li&gt;
&lt;li&gt;Prefix-scoped S3 policies reduce damage if an instance is compromised.&lt;/li&gt;
&lt;li&gt;Terraform makes role and bucket permissions reviewable in pull requests.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Project on GitHub
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/rufilboss/terraform-ec2-s3-iam-role" rel="noopener noreferrer"&gt;Terraform project on GitHub&lt;/a&gt; with EC2, IAM role, and S3 upload policy.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://registry.terraform.io/providers/hashicorp/aws/latest/docs" rel="noopener noreferrer"&gt;Terraform AWS Provider documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html" rel="noopener noreferrer"&gt;IAM roles for Amazon EC2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_examples_s3.html" rel="noopener noreferrer"&gt;Granting permissions to access Amazon S3 resources&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;Amazon S3 security best practices&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html" rel="noopener noreferrer"&gt;Instance metadata and IAM roles&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>terraform</category>
      <category>aws</category>
      <category>ec2</category>
      <category>s3</category>
    </item>
    <item>
      <title>Secret Scanning in CI: What Pre-Commit, Pull Request, and Main Branch Each Actually Catch</title>
      <dc:creator>Ilyas Rufai</dc:creator>
      <pubDate>Wed, 17 Jun 2026 11:32:18 +0000</pubDate>
      <link>https://dev.to/rufilboss/secret-scanning-in-ci-what-pre-commit-pull-request-and-main-branch-each-actually-catch-3c55</link>
      <guid>https://dev.to/rufilboss/secret-scanning-in-ci-what-pre-commit-pull-request-and-main-branch-each-actually-catch-3c55</guid>
      <description>&lt;p&gt;A teammate pastes an AWS access key into a PR comment to "debug quickly." Another commits &lt;code&gt;.env.production&lt;/code&gt; because &lt;code&gt;.gitignore&lt;/code&gt; was wrong on a new microservice. A third rotates nothing after a contractor laptop compromise because "we never committed secrets, probably fine."&lt;/p&gt;

&lt;p&gt;Secret scanners are not interchangeable at every stage of the software development lifecycle (SDLC). Running only on &lt;code&gt;main&lt;/code&gt; means the secret already lived in git history. Running only locally means it never ran on the machine that mattered. &lt;strong&gt;Layers&lt;/strong&gt; matter, and each layer should have a different job.&lt;/p&gt;

&lt;p&gt;In this article, you will implement a practical three-layer secret-scanning model with Gitleaks and GitHub Actions, then verify each layer and handle real incidents without creating scanner fatigue.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who this is for:&lt;/strong&gt; Engineers owning GitHub Actions security checks for application repos.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you'll build:&lt;/strong&gt; Pre-commit hook, PR scan, and post-merge history scan with shared allowlist discipline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites:&lt;/strong&gt; Git, GitHub repo, optional Gitleaks binary locally.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Use three layers with different goals: pre-commit for fast local feedback, pull request as a merge gate, and scheduled default-branch scans for hygiene.&lt;/li&gt;
&lt;li&gt;Block merges on PR findings; treat default-branch findings as incidents plus historical cleanup.&lt;/li&gt;
&lt;li&gt;Share one &lt;code&gt;.gitleaks.toml&lt;/code&gt; across local and CI runs, and allowlist only specific safe paths.&lt;/li&gt;
&lt;li&gt;Rotation and revocation matter more than history rewrite; deleting git history without credential rotation does not contain compromise.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why one scanner in one place fails
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Catches&lt;/th&gt;
&lt;th&gt;Misses&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pre-commit&lt;/td&gt;
&lt;td&gt;Keys before they enter any remote&lt;/td&gt;
&lt;td&gt;Skipped hooks (&lt;code&gt;--no-verify&lt;/code&gt;), new clones without hooks installed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pull request&lt;/td&gt;
&lt;td&gt;Keys introduced in the diff; blocks merge&lt;/td&gt;
&lt;td&gt;Secrets only in comments, wiki, or release assets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Default branch/history&lt;/td&gt;
&lt;td&gt;Deep scan, scheduled; finds old leaks&lt;/td&gt;
&lt;td&gt;Still too late for keys already exfiltrated from an open PR&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Treat findings like production incidents at the PR layer; treat main-branch scans as &lt;strong&gt;hygiene and audit evidence&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 1 — Pre-commit (fast feedback)
&lt;/h2&gt;

&lt;p&gt;Start with local feedback so engineers catch leaks before pushing.&lt;br&gt;
Install &lt;a href="https://github.com/gitleaks/gitleaks" rel="noopener noreferrer"&gt;Gitleaks&lt;/a&gt; and wire a hook:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# .pre-commit-config.yaml&lt;/span&gt;
repos:
  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.21.2
    hooks:
      - &lt;span class="nb"&gt;id&lt;/span&gt;: gitleaks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pre-commit &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Developers see failures in under two seconds on staged files. &lt;strong&gt;Allowlist&lt;/strong&gt; test fixtures explicitly, never disable rules globally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="c"&gt;# .gitleaks.toml (repo root)&lt;/span&gt;
&lt;span class="nn"&gt;[allowlist]&lt;/span&gt;
&lt;span class="py"&gt;paths&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="s"&gt;'''^tests/fixtures/'''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s"&gt;'''^docs/examples/fake-credentials\.json$'''&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Rule:&lt;/strong&gt; If a path needs a real secret for tests, use a vault-injected env var in CI—not a committed file with "fake" in the name and a real-looking key format.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 2 — Pull request scan (merge gate)
&lt;/h2&gt;

&lt;p&gt;The PR layer is your enforcement point. Scan &lt;strong&gt;only the PR diff&lt;/strong&gt; so developers are not punished for historical debt on day one:&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Secret scan (PR)&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&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;pull-requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;gitleaks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;fetch-depth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&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;Run Gitleaks on PR diff&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gitleaks/gitleaks-action@v2&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;GITHUB_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;config-path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.gitleaks.toml&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Configure branch protection: this check is required before the merge.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fork PRs:&lt;/strong&gt; &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; from forks is read-only; gitleaks-action still scans the diff but cannot comment with elevated permissions, acceptable for public repos; for private repos, consider org-level secret scanning (GitHub Advanced Security) as a parallel signal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 3 — Main branch and history (hygiene)
&lt;/h2&gt;

&lt;p&gt;The default-branch layer catches historical debt and supply-chain surprises.&lt;br&gt;
Run a weekly full-history scan:&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Secret scan (history)&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;6&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;1"&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;gitleaks-history&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;fetch-depth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gitleaks/gitleaks-action@v2&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;config-path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.gitleaks.toml&lt;/span&gt;
          &lt;span class="c1"&gt;# no PR context: full repo scan&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When this fires on &lt;code&gt;main&lt;/code&gt;, assume rotation: revoke the credential, purge from history (&lt;code&gt;git filter-repo&lt;/code&gt; or BFG) only &lt;strong&gt;after&lt;/strong&gt; revocation—scrubbing git without rotating the secret fixes nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to verify this works
&lt;/h2&gt;

&lt;p&gt;Use these checks to confirm each layer is doing the right job:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pre-commit check:&lt;/strong&gt; add a fake test secret string in a staged file and confirm &lt;code&gt;pre-commit&lt;/code&gt; blocks the commit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PR gate check:&lt;/strong&gt; Open a test pull request with the same string and confirm the workflow fails before merge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;History scan check:&lt;/strong&gt; run &lt;code&gt;workflow_dispatch&lt;/code&gt; for the history workflow and confirm it scans the full repository.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Allowlist check:&lt;/strong&gt; Place the same test token in an allowlisted fixture path and confirm scanner behavior matches your policy.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Use clearly fake values in tests and examples. Never use production-like secrets for scanner testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reducing false positives without going blind
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Allowlist paths, not regexes for "AWS"&lt;/strong&gt; — Broad entropy exceptions hide real keys.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separate example keys:&lt;/strong&gt; Use obviously invalid formats (&lt;code&gt;AKIAFAKE00000000000&lt;/code&gt;) in docs; scanners and humans both win.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom rules sparingly:&lt;/strong&gt; Add org-specific patterns (internal API key prefix) via Gitleaks &lt;code&gt;[[rules]]&lt;/code&gt;; do not fork the entire ruleset unless you can maintain it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Teach the escape hatch:&lt;/strong&gt; If pre-commit blocks a docs change, fix the example, not &lt;code&gt;SKIP=gitleaks&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  When a secret is found anyway
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Revoke&lt;/strong&gt; in the provider (AWS, Stripe, Slack) immediately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rotate&lt;/strong&gt; dependent systems; assume compromise if the repo is public or the PR was open.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Purge history&lt;/strong&gt; if the secret touched git; coordinate with legal/comms if customer data access was possible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Post-incident:&lt;/strong&gt; add a rule or allowlist fix so the same mistake is a one-liner next time.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Pair this scanning pipeline with short-lived cloud credentials (for example, OIDC-based CI federation) so leaked CI identity material has reduced blast radius.&lt;/p&gt;

&lt;h2&gt;
  
  
  When this breaks down
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Teams can bypass local hooks, so pre-commit cannot be your enforcement layer by itself.&lt;/li&gt;
&lt;li&gt;Diff-only PR scans do not catch existing secrets already in history, release assets, or external systems.&lt;/li&gt;
&lt;li&gt;Aggressive allowlists can silently reduce coverage if they are not reviewed during security change management.&lt;/li&gt;
&lt;li&gt;Scanner success can create false confidence if incident response, revocation, and rotation are weak.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Use three layers: pre-commit (speed), PR (gate), scheduled main (history).&lt;/li&gt;
&lt;li&gt;Share one &lt;code&gt;.gitleaks.toml&lt;/code&gt;; allowlist paths, not categories of secrets.&lt;/li&gt;
&lt;li&gt;Block merge on PR findings; treat main-branch hits as incident + hygiene work.&lt;/li&gt;
&lt;li&gt;Rotation beats history rewriting; scanning is detection, not prevention alone.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/gitleaks/gitleaks#configuration" rel="noopener noreferrer"&gt;Gitleaks configuration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning" rel="noopener noreferrer"&gt;GitHub secret scanning&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/removing-sensitive-data-from-a-repository" rel="noopener noreferrer"&gt;Removing sensitive data from a repository&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devsecops</category>
      <category>githubactions</category>
      <category>gitleaks</category>
      <category>secrets</category>
    </item>
    <item>
      <title>Amazon S3 Bucket Names Aren’t Always Globally Unique Anymore — Here’s What Changed (and Why I’m Excited)</title>
      <dc:creator>Ilyas Rufai</dc:creator>
      <pubDate>Thu, 19 Mar 2026 11:54:35 +0000</pubDate>
      <link>https://dev.to/rufilboss/amazon-s3-bucket-names-arent-always-globally-unique-anymore-heres-what-changed-and-why-im-3a1f</link>
      <guid>https://dev.to/rufilboss/amazon-s3-bucket-names-arent-always-globally-unique-anymore-heres-what-changed-and-why-im-3a1f</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F74tcw6c7amqslpu9mfz6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F74tcw6c7amqslpu9mfz6.png" alt="Global namespace" width="800" height="287"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgj70v2rg5nh1b86zsuyc.png" alt="Account regional namespace" width="800" height="350"&gt;
&lt;/h2&gt;

&lt;p&gt;I still remember the first time S3 humbled me.&lt;/p&gt;

&lt;p&gt;I was following a “hello AWS” tutorial, feeling confident, and then S3 hit me with:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Bucket name already exists&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I tried again. And again. And again.&lt;br&gt;&lt;br&gt;
Different names, different variations, still taken!&lt;/p&gt;

&lt;p&gt;That’s when I learned the “classic rule”:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;S3 bucket names are globally unique&lt;/strong&gt; (within an AWS partition).&lt;br&gt;&lt;br&gt;
Meaning: if someone anywhere already owns that bucket name, you can’t have it.&lt;/p&gt;

&lt;p&gt;But here’s the big update I recently learned, and it’s &lt;em&gt;huge&lt;/em&gt; for builders, platform teams, and security folks:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Amazon S3 now supports creating general-purpose buckets inside an “account regional namespace”, which removes the “find a globally unique name” pain.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;AWS announced this on &lt;strong&gt;Mar 12, 2026&lt;/strong&gt;:&lt;br&gt;
&lt;code&gt;https://aws.amazon.com/about-aws/whats-new/2026/03/amazon-s3-account-regional-namespaces/&lt;/code&gt;&lt;/p&gt;



&lt;p&gt;In this article, I’ll break down what changed, how it works, why it matters, and how I’d explain it if I were designing bucket naming at scale.&lt;/p&gt;
&lt;h2&gt;
  
  
  What used to be true (and is still true by default)
&lt;/h2&gt;

&lt;p&gt;By default, &lt;strong&gt;general purpose buckets exist in a global namespace&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;AWS puts it plainly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;“Each bucket name must be unique across all AWS accounts in all the AWS Regions within a partition.”&lt;/strong&gt;
Official docs: &lt;code&gt;https://docs.aws.amazon.com/AmazonS3/latest/userguide/gpbucketnamespaces.html&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;my-company-logs&lt;/code&gt; can only exist once (in that partition).&lt;/li&gt;
&lt;li&gt;If the bucket is deleted, the name becomes available again — and someone else could recreate it later.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcwy76sxvn8ixkt227rjv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcwy76sxvn8ixkt227rjv.png" alt="Bucket already exists" width="799" height="265"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  What changed: Account regional namespaces (Mar 2026)
&lt;/h2&gt;

&lt;p&gt;AWS introduced a second option:&lt;/p&gt;

&lt;p&gt;You can now create a &lt;strong&gt;general purpose bucket&lt;/strong&gt; in a namespace reserved only for your account:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;“You can also choose to create a bucket in your account regional namespace. Your account regional namespace is a subdivision of the global namespace that only your account can create buckets in.”&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Official docs: &lt;code&gt;https://docs.aws.amazon.com/AmazonS3/latest/userguide/gpbucketnamespaces.html&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;AWS also summarizes the “why” in the announcement:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;eliminates the need to find globally unique bucket names
&lt;/li&gt;
&lt;li&gt;makes it easier to build workloads like “one bucket per customer/team/dataset”
&lt;/li&gt;
&lt;li&gt;can be enforced using IAM policies/service control policies (SCPs)
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Announcement: &lt;code&gt;https://aws.amazon.com/about-aws/whats-new/2026/03/amazon-s3-account-regional-namespaces/&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The nuance I want to keep clear:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Global namespace is still the default&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;But now you have a &lt;strong&gt;reserved&lt;/strong&gt; namespace you can opt into&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  How the naming looks (the new naming convention)
&lt;/h2&gt;

&lt;p&gt;Buckets in your &lt;strong&gt;account regional namespace&lt;/strong&gt; follow this naming pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bucket-name-prefix-accountId-region-an
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;amzn-s3-demo-bucket-111122223333-us-west-2-an
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Source: &lt;code&gt;https://docs.aws.amazon.com/AmazonS3/latest/userguide/gpbucketnamespaces.html&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;So instead of fighting to get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;my-company-logs&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;…you can choose a predictable prefix like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;my-company-logs&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;and the final name will include your account + region suffix that makes it unique &lt;em&gt;to you&lt;/em&gt;.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Why this matters (in real systems)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1) It kills the “I spent 20 minutes naming a bucket” problem
&lt;/h3&gt;

&lt;p&gt;If you’ve ever created buckets for different environments, you know this loop:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;myapp-prod-logs&lt;/code&gt; (taken)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;myapp-prod-logs-1&lt;/code&gt; (taken)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;myapp-prod-logs-2026&lt;/code&gt; (taken)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;myapp-prod-logs-&amp;lt;random&amp;gt;&lt;/code&gt; (finally works, but now it’s ugly)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Account regional namespaces support a much more predictable naming scheme.&lt;/p&gt;

&lt;h3&gt;
  
  
  2) It reduces risk after deletion (a security win)
&lt;/h3&gt;

&lt;p&gt;In the shared global namespace, once a bucket is deleted, the name becomes available again. Another account can recreate the name later and potentially receive requests intended for that name.&lt;/p&gt;

&lt;p&gt;Official docs: &lt;code&gt;https://docs.aws.amazon.com/AmazonS3/latest/userguide/gpbucketnamespaces.html&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;In contrast, for account regional namespace buckets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Only your account can create buckets in that namespace&lt;/li&gt;
&lt;li&gt;Another account cannot recreate your account-regional bucket name&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Official docs: &lt;code&gt;https://docs.aws.amazon.com/AmazonS3/latest/userguide/gpbucketnamespaces.html&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3) It scales for bucket-per-customer / bucket-per-team designs
&lt;/h3&gt;

&lt;p&gt;AWS explicitly calls out bucket-per-customer/team/dataset workloads in the announcement:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;https://aws.amazon.com/about-aws/whats-new/2026/03/amazon-s3-account-regional-namespaces/&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;That’s a very modern SaaS/platform pattern, and it’s nice to see S3 making it easier to do it safely and predictably.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating an account-regional namespace bucket (AWS CLI)
&lt;/h2&gt;

&lt;p&gt;The official docs show creating a bucket using the AWS CLI like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws s3api create-bucket &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--bucket&lt;/span&gt; amzn-s3-demo-bucket-012345678910-us-west-1-an &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--bucket-namespace&lt;/span&gt; account-regional &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--region&lt;/span&gt; us-west-1 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--create-bucket-configuration&lt;/span&gt; &lt;span class="nv"&gt;LocationConstraint&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;us-west-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Source: &lt;code&gt;https://docs.aws.amazon.com/AmazonS3/latest/userguide/gpbucketnamespaces.html&lt;/code&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Before you run the command, make sure your &lt;code&gt;aws cli&lt;/code&gt; command is updated.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  How I’d enforce this in an organization (IAM/SCP policy)
&lt;/h2&gt;

&lt;p&gt;If I were working on a platform team, I’d want consistent naming and fewer security foot-guns. AWS provides a clean enforcement pattern using the condition key:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;s3:x-amz-bucket-namespace&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here’s an example IAM policy from the docs that denies &lt;code&gt;s3:CreateBucket&lt;/code&gt; unless the request is using &lt;code&gt;account-regional&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"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;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"RequireAccountRegionalBucketCreation"&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;"Deny"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"s3:CreateBucket"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"StringNotEquals"&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;"s3:x-amz-bucket-namespace"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"account-regional"&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;Source: &lt;code&gt;https://docs.aws.amazon.com/AmazonS3/latest/userguide/gpbucketnamespaces.html&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Bucket naming rules (still important)
&lt;/h2&gt;

&lt;p&gt;Even with namespaces, S3 bucket names still follow the general naming rules, like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;3–63 characters
&lt;/li&gt;
&lt;li&gt;lowercase letters, numbers, periods (&lt;code&gt;.&lt;/code&gt;), and hyphens (&lt;code&gt;-&lt;/code&gt;) only
&lt;/li&gt;
&lt;li&gt;must start and end with a letter or number
&lt;/li&gt;
&lt;li&gt;no two adjacent periods
&lt;/li&gt;
&lt;li&gt;cannot look like an IP address
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Official rules: &lt;code&gt;https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucketnamingrules.html&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Also note:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;-an&lt;/code&gt; is reserved for account regional namespace buckets
&lt;/li&gt;
&lt;li&gt;the account/regional suffix counts toward the 63-character limit
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Official docs: &lt;code&gt;https://docs.aws.amazon.com/AmazonS3/latest/userguide/gpbucketnamespaces.html&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The mental model I use
&lt;/h2&gt;

&lt;p&gt;This is how I explain it to myself:&lt;/p&gt;

&lt;h3&gt;
  
  
  Global namespace (default)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Like a global username system
&lt;/li&gt;
&lt;li&gt;If someone owns it, you can’t use it
&lt;/li&gt;
&lt;li&gt;If deleted, someone else might grab it later
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Account regional namespace (new)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Like a reserved “username space” under &lt;em&gt;my account + my region&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;I still choose the prefix, but uniqueness is guaranteed by the suffix
&lt;/li&gt;
&lt;li&gt;Another account can’t recreate my account-regional bucket name
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final takeaway
&lt;/h2&gt;

&lt;p&gt;When I first learned S3 bucket names had to be globally unique, I accepted it as “just an AWS thing.”&lt;/p&gt;

&lt;p&gt;But account regional namespaces feel like AWS responding to how people actually build today: lots of environments, lots of teams, and a need for predictable and secure naming.&lt;/p&gt;

&lt;p&gt;If I’m building something new, I’d strongly consider defaulting new buckets to the account regional namespace — especially if naming conventions matter, or if I want to reduce the risk of name reuse after deletion.&lt;/p&gt;




&lt;h2&gt;
  
  
  References (official)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;AWS announcement (Mar 12, 2026): &lt;code&gt;https://aws.amazon.com/about-aws/whats-new/2026/03/amazon-s3-account-regional-namespaces/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Namespaces for general purpose buckets: &lt;code&gt;https://docs.aws.amazon.com/AmazonS3/latest/userguide/gpbucketnamespaces.html&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;General purpose bucket naming rules: &lt;code&gt;https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucketnamingrules.html&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>s3</category>
      <category>cloud</category>
      <category>devops</category>
    </item>
    <item>
      <title>I built a zero-cost end-to-end DevOps pipeline (GitHub Actions + Docker + Kubernetes + Docker Hub)</title>
      <dc:creator>Ilyas Rufai</dc:creator>
      <pubDate>Wed, 04 Mar 2026 11:27:47 +0000</pubDate>
      <link>https://dev.to/rufilboss/i-built-a-zero-cost-end-to-end-devops-pipeline-github-actions-docker-kubernetes-docker-hub-2g9n</link>
      <guid>https://dev.to/rufilboss/i-built-a-zero-cost-end-to-end-devops-pipeline-github-actions-docker-kubernetes-docker-hub-2g9n</guid>
      <description>&lt;p&gt;I just finished a small but &lt;strong&gt;real&lt;/strong&gt; DevOps project and I want to share it in case you’re trying to build your own portfolio.&lt;/p&gt;

&lt;p&gt;The idea was simple: &lt;strong&gt;take a tiny app and wire the whole path from &lt;code&gt;git push&lt;/code&gt; → CI/CD → container registry → Kubernetes&lt;/strong&gt;, without paying for any cloud resources.&lt;/p&gt;

&lt;p&gt;You can grab the code here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub repo&lt;/strong&gt;: &lt;code&gt;https://github.com/rufilboss/devops-e2e-pipeline&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker Hub image&lt;/strong&gt;: &lt;code&gt;docker.io/asruf/demo-app:latest&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What I built (high level)
&lt;/h2&gt;

&lt;p&gt;Concretely, the project contains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;App&lt;/strong&gt;: Tiny Flask API (&lt;code&gt;app/main.py&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Container&lt;/strong&gt;: Dockerfile (&lt;code&gt;app/Dockerfile&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI/CD&lt;/strong&gt;: GitHub Actions workflow that builds and pushes images to &lt;strong&gt;Docker Hub&lt;/strong&gt; (&lt;code&gt;.github/workflows/ci-cd.yaml&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kubernetes&lt;/strong&gt;: &lt;code&gt;Deployment&lt;/code&gt; + &lt;code&gt;Service&lt;/code&gt; (&lt;code&gt;k8s/*.yaml&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Terraform (optional)&lt;/strong&gt;: creates the Kubernetes namespace (&lt;code&gt;terraform/*.tf&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything here runs &lt;strong&gt;for free&lt;/strong&gt; on a local cluster (kind or minikube) and a free Docker Hub + GitHub account.&lt;/p&gt;




&lt;h2&gt;
  
  
  Prerequisites I used
&lt;/h2&gt;

&lt;p&gt;To follow exactly what I did, you’ll want:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Git + GitHub repo&lt;/li&gt;
&lt;li&gt;Docker&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kubectl&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;One local Kubernetes option:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;kind&lt;/strong&gt; (what I used), or&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;minikube&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Terraform (optional, only for the IaC part)&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;Docker Hub&lt;/strong&gt; account (I used mine &lt;code&gt;asruf&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Project layout
&lt;/h2&gt;

&lt;p&gt;This is the layout of the repo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;devops-e2e-pipeline/
├── app
│   ├── Dockerfile
│   ├── main.py
│   └── requirements.txt
├── k8s
│   ├── namespace.yaml
│   └── deployment.yaml
├── terraform
│   ├── main.tf
│   └── k8s.tf
└── .github
    └── workflows
        └── ci-cd.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  1) The app I used (simple Flask service)
&lt;/h2&gt;

&lt;p&gt;I deliberately kept the app tiny so the focus is on the &lt;strong&gt;pipeline&lt;/strong&gt;, not the code.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/&lt;/code&gt; — info about the service (name, version, env, status)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/health&lt;/code&gt; — liveness&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/ready&lt;/code&gt; — readiness&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;app/main.py&lt;/code&gt;:&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;flask&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jsonify&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;VERSION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;APP_VERSION&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;1.0.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ENV&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ENV&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;dev&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@app.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;index&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;jsonify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;service&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;demo-app&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;version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;VERSION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;env&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ENV&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;ok&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="nd"&gt;@app.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/health&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;health&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;jsonify&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;healthy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;

&lt;span class="nd"&gt;@app.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/ready&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;jsonify&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;ready&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.0.0.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dependencies:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;app/requirements.txt&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flask&amp;gt;=3.0.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The app listens on &lt;strong&gt;port 8080&lt;/strong&gt;, which I re-use everywhere (Docker, Kubernetes, port-forward, etc.).&lt;/p&gt;




&lt;h2&gt;
  
  
  2) Containerizing it with Docker
&lt;/h2&gt;

&lt;p&gt;My Dockerfile is intentionally straightforward but shows some basic good practices:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Slim base image&lt;/li&gt;
&lt;li&gt;Non-root user&lt;/li&gt;
&lt;li&gt;Requirements installed in their own layer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;app/Dockerfile&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;python:3.12-slim&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;runtime&lt;/span&gt;

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;adduser &lt;span class="nt"&gt;--disabled-password&lt;/span&gt; &lt;span class="nt"&gt;--gecos&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt; appuser

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; requirements.txt .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-cache-dir&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; pip freeze &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; requirements.lock

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; main.py .&lt;/span&gt;

&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; appuser&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 8080&lt;/span&gt;

&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; FLASK_APP=main.py&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["python", "-m", "flask", "run", "--host=0.0.0.0", "--port=8080"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Local sanity check
&lt;/h3&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;devops-e2e-pipeline

docker build &lt;span class="nt"&gt;-t&lt;/span&gt; demo-app:local ./app
docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 8080:8080 &lt;span class="nt"&gt;--name&lt;/span&gt; demo-app-test demo-app:local

&lt;span class="c"&gt;# In another terminal:&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://localhost:8080/health
curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://localhost:8080/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gave me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;{"status": "healthy"}&lt;/code&gt; from &lt;code&gt;/health&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;{"env":"dev","service":"demo-app","status":"ok","version":"1.0.0"}&lt;/code&gt; from &lt;code&gt;/&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once that worked, I moved on to Kubernetes.&lt;/p&gt;

&lt;p&gt;Here are the screenshots from my terminal while doing this:&lt;/p&gt;

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

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

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

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




&lt;h2&gt;
  
  
  3) Running it on Kubernetes (kind or minikube)
&lt;/h2&gt;

&lt;p&gt;I wanted a “real” deployment with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A dedicated namespace&lt;/li&gt;
&lt;li&gt;2 replicas&lt;/li&gt;
&lt;li&gt;Liveness/readiness probes&lt;/li&gt;
&lt;li&gt;Resource requests/limits&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Starting a local cluster
&lt;/h3&gt;

&lt;p&gt;You can use either tool; I used &lt;strong&gt;kind&lt;/strong&gt;, but here are both options.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;minikube:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;minikube start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;kind:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kind create cluster &lt;span class="nt"&gt;--name&lt;/span&gt; demo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here’s what that looked like for me:&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Making the image visible to the cluster
&lt;/h3&gt;

&lt;p&gt;Kubernetes can’t automatically see &lt;code&gt;demo-app:local&lt;/code&gt; unless you either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;build inside the cluster’s Docker daemon (minikube), or&lt;/li&gt;
&lt;li&gt;load the image into kind.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Option A: minikube&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;minikube docker-env&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
docker build &lt;span class="nt"&gt;-t&lt;/span&gt; demo-app:local ./app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Option B: kind&lt;/strong&gt; (what I used):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker build &lt;span class="nt"&gt;-t&lt;/span&gt; demo-app:local ./app
kind load docker-image demo-app:local &lt;span class="nt"&gt;--name&lt;/span&gt; demo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the &lt;code&gt;kind load&lt;/code&gt; output:&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Kubernetes manifests I used
&lt;/h3&gt;

&lt;p&gt;Namespace:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;k8s/namespace.yaml&lt;/code&gt;:&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;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Namespace&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&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;demo-app&lt;/span&gt;
  &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;app.kubernetes.io/name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;demo-app&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deployment + Service:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;k8s/deployment.yaml&lt;/code&gt;:&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;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deployment&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&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;demo-app&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;demo-app&lt;/span&gt;
  &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;demo-app&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;demo-app&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;demo-app&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&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;app&lt;/span&gt;
          &lt;span class="c1"&gt;# Local image for kind/minikube:&lt;/span&gt;
          &lt;span class="c1"&gt;# image: demo-app:local&lt;/span&gt;
          &lt;span class="c1"&gt;# Docker Hub image (asruf/demo-app) when using CI/CD:&lt;/span&gt;
          &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker.io/asruf/demo-app:latest&lt;/span&gt;
          &lt;span class="na"&gt;imagePullPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;IfNotPresent&lt;/span&gt;
          &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;containerPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&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;http&lt;/span&gt;
          &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&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;ENV&lt;/span&gt;
              &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;production"&lt;/span&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;APP_VERSION&lt;/span&gt;
              &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1.0.0"&lt;/span&gt;
          &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;50m&lt;/span&gt;
              &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;64Mi&lt;/span&gt;
            &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;200m&lt;/span&gt;
              &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;128Mi&lt;/span&gt;
          &lt;span class="na"&gt;livenessProbe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;httpGet&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/health&lt;/span&gt;
              &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;
            &lt;span class="na"&gt;initialDelaySeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
            &lt;span class="na"&gt;periodSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
          &lt;span class="na"&gt;readinessProbe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;httpGet&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/ready&lt;/span&gt;
              &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;
            &lt;span class="na"&gt;initialDelaySeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
            &lt;span class="na"&gt;periodSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Service&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&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;demo-app&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;demo-app&lt;/span&gt;
  &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;demo-app&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ClusterIP&lt;/span&gt;
  &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;
      &lt;span class="na"&gt;targetPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;
      &lt;span class="na"&gt;protocol&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;TCP&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;http&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;demo-app&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Applying and testing
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; k8s/namespace.yaml
kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; k8s/deployment.yaml

kubectl get pods,svc &lt;span class="nt"&gt;-n&lt;/span&gt; demo-app

kubectl port-forward &lt;span class="nt"&gt;-n&lt;/span&gt; demo-app svc/demo-app 8080:80
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then in another terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://localhost:8080/health
curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://localhost:8080/ready
curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://localhost:8080/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here’s the &lt;code&gt;kubectl apply&lt;/code&gt; / &lt;code&gt;kubectl get&lt;/code&gt; snapshot:&lt;/p&gt;

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

&lt;p&gt;And the port-forward:&lt;/p&gt;

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

&lt;p&gt;At this point, I had the app running as &lt;strong&gt;2 replicas in a local cluster&lt;/strong&gt;, fronted by a Service, with working probes.&lt;/p&gt;




&lt;h2&gt;
  
  
  4) Pushing to Docker Hub
&lt;/h2&gt;

&lt;p&gt;My Docker Hub username is &lt;strong&gt;&lt;code&gt;asruf&lt;/code&gt;&lt;/strong&gt;. I first pushed manually to make sure everything worked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker tag demo-app:local asruf/demo-app:latest
docker push asruf/demo-app:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that, the image was available at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;docker.io/asruf/demo-app:latest&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s the image the Kubernetes manifest uses by default in this repo.&lt;/p&gt;




&lt;h2&gt;
  
  
  5) CI/CD with GitHub Actions → Docker Hub
&lt;/h2&gt;

&lt;p&gt;I wanted the pipeline to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Build the image on every push / PR&lt;/li&gt;
&lt;li&gt;Push to Docker Hub on pushes (not PRs)&lt;/li&gt;
&lt;li&gt;Tag images with:

&lt;ul&gt;
&lt;li&gt;the commit SHA&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;latest&lt;/code&gt; (for the default branch)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The workflow is at &lt;code&gt;./.github/workflows/ci-cd.yaml&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Docker Hub secrets
&lt;/h3&gt;

&lt;p&gt;In my GitHub repo I created 2 &lt;strong&gt;Actions secrets&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;DOCKERHUB_USERNAME&lt;/code&gt; — &lt;code&gt;asruf&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DOCKERHUB_TOKEN&lt;/code&gt; — a Docker Hub access token&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can find these in:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;GitHub repo → Settings → Secrets and variables → Actions&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  What the workflow does
&lt;/h3&gt;

&lt;p&gt;High level:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Check out code&lt;/li&gt;
&lt;li&gt;Set up Buildx&lt;/li&gt;
&lt;li&gt;Log in to Docker Hub with &lt;code&gt;DOCKERHUB_USERNAME&lt;/code&gt; + &lt;code&gt;DOCKERHUB_TOKEN&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Build the Docker image from &lt;code&gt;./app&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Tag it with SHA + &lt;code&gt;latest&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Push to &lt;code&gt;docker.io/asruf/demo-app&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So every push to &lt;code&gt;main&lt;/code&gt; automatically gives me a fresh image on Docker Hub, ready for Kubernetes.&lt;/p&gt;




&lt;h2&gt;
  
  
  6) Optional: Terraform for the namespace
&lt;/h2&gt;

&lt;p&gt;I also wanted at least one &lt;strong&gt;Infrastructure as Code&lt;/strong&gt; piece in here, so I used Terraform’s Kubernetes provider to create the namespace.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;terraform/main.tf&lt;/code&gt; (provider + versions) and &lt;code&gt;terraform/k8s.tf&lt;/code&gt; (namespace resource) are already in the repo.&lt;/p&gt;

&lt;p&gt;If your &lt;code&gt;~/.kube/config&lt;/code&gt; points at a running cluster:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;terraform

terraform init
terraform plan
terraform apply
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is small on purpose, but it’s enough to say &lt;strong&gt;“I manage part of the Kubernetes infrastructure with Terraform”&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here’s what my &lt;code&gt;terraform init&lt;/code&gt; + &lt;code&gt;terraform plan&lt;/code&gt; looked like:&lt;/p&gt;

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




&lt;h2&gt;
  
  
  7) How can you reuse this
&lt;/h2&gt;

&lt;p&gt;If you want to adapt this project for yourself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fork the repo or copy the layout&lt;/li&gt;
&lt;li&gt;Change the &lt;strong&gt;Docker Hub&lt;/strong&gt; username and repo name&lt;/li&gt;
&lt;li&gt;Update:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;k8s/deployment.yaml&lt;/code&gt; &lt;code&gt;image:&lt;/code&gt; field&lt;/li&gt;
&lt;li&gt;GitHub Actions secrets (&lt;code&gt;DOCKERHUB_USERNAME&lt;/code&gt;, &lt;code&gt;DOCKERHUB_TOKEN&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Swap the Flask app for your own service if you like&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The nice part is that the pattern stays the same:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;App → Docker → Docker Hub → Kubernetes → (optional) Terraform&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once this pipeline is in your portfolio, you can honestly tell people:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“I’ve built and maintained an end-to-end CI/CD pipeline with GitHub Actions, Docker, Kubernetes, Docker Hub, and Terraform. Here’s the repo and here’s the running app.”&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;This project is small, but it touches a lot of the buzzwords you see in job posts and freelance gigs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub Actions&lt;/li&gt;
&lt;li&gt;Docker&lt;/li&gt;
&lt;li&gt;Docker Hub&lt;/li&gt;
&lt;li&gt;Kubernetes&lt;/li&gt;
&lt;li&gt;Terraform&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’re trying to break into DevOps or just want something concrete to show, feel free to &lt;strong&gt;clone my &lt;a href="https://github.com/rufilboss/devops-e2e-pipeline" rel="noopener noreferrer"&gt;repo&lt;/a&gt;, run it locally, and then customize it&lt;/strong&gt; to match your own style and stack.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>cicd</category>
      <category>docker</category>
      <category>kubernetes</category>
    </item>
    <item>
      <title>Are You Planning to Get Into DevOps in 2026? A Practical Guide for the Real World</title>
      <dc:creator>Ilyas Rufai</dc:creator>
      <pubDate>Wed, 24 Dec 2025 21:57:09 +0000</pubDate>
      <link>https://dev.to/rufilboss/are-you-planning-to-get-into-devops-in-2026-a-practical-guide-for-the-real-world-3blb</link>
      <guid>https://dev.to/rufilboss/are-you-planning-to-get-into-devops-in-2026-a-practical-guide-for-the-real-world-3blb</guid>
      <description>&lt;p&gt;It’s &lt;strong&gt;December 2025&lt;/strong&gt;, almost January 2026, and if you’re asking yourself &lt;em&gt;“Should I start DevOps now?”&lt;/em&gt; Then this article is for you.&lt;/p&gt;

&lt;p&gt;Not because DevOps is &lt;em&gt;trending&lt;/em&gt; (it is, most companies now practice DevOps workflows and advanced automation standards), but because the &lt;em&gt;demand for real DevOps skill — not surface-level tool knowledge —&lt;/em&gt; is only going to grow faster in the AI-augmented era we’re entering.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Now is the Right Time (But Also the Hardest Time)
&lt;/h2&gt;

&lt;p&gt;Over the last five years, DevOps has shifted from pure automation to &lt;strong&gt;intelligent automation&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitOps — treating Git as the &lt;em&gt;single source of truth&lt;/em&gt; for infrastructure and app deployments — is becoming mainstream.&lt;/li&gt;
&lt;li&gt;Cloud-native, serverless, and event-driven designs are reshaping how systems are built.&lt;/li&gt;
&lt;li&gt;AI is now embedded in pipelines, observability, and even predictive deployments, automating decisions that used to require senior engineers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The payoff? Teams deploy faster, with fewer errors, and automation frees engineers from repetitive toil.&lt;br&gt;
The catch? &lt;strong&gt;AI can mask gaps in understanding.&lt;/strong&gt; You can have something running and still not know &lt;em&gt;why&lt;/em&gt; it runs, and that’s where most beginners crash.⁣&lt;/p&gt;

&lt;p&gt;In 2026, &lt;em&gt;DevOps without deep fundamentals&lt;/em&gt; is like driving a car you can’t fix when it breaks.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Works (Based on Real Industry Signals)
&lt;/h2&gt;

&lt;p&gt;Here’s what industry trends say about DevOps evolution:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitOps adoption&lt;/strong&gt; is expected to grow sharply, improving reliability and consistency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DevSecOps&lt;/strong&gt; — embedding security into every pipeline is essential, not optional.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observers and AI-powered observability tools&lt;/strong&gt; help engineers find problems before they hit users.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Serverless pipelines&lt;/strong&gt; are reshaping how we think about infrastructure abstraction and event wiring.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These aren’t buzzwords; they are what you’ll see in job descriptions, production systems, and real-world infrastructure teams.&lt;/p&gt;




&lt;h2&gt;
  
  
  But First — A Reality Check
&lt;/h2&gt;

&lt;p&gt;Before we talk tools: DevOps is &lt;strong&gt;not a checklist&lt;/strong&gt; of technologies. It’s a &lt;em&gt;cultural and engineering mindset&lt;/em&gt; rooted in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Continuous Delivery&lt;/li&gt;
&lt;li&gt;Automated, high-confidence pipelines&lt;/li&gt;
&lt;li&gt;Reproducible infrastructure&lt;/li&gt;
&lt;li&gt;Collaboration between development and operations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AI will help you generate configs, but it &lt;strong&gt;won’t give you intuition&lt;/strong&gt; around:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why a pipeline failed&lt;/li&gt;
&lt;li&gt;How a Kubernetes pod behaves under pressure&lt;/li&gt;
&lt;li&gt;What happens when your Terraform state drifts&lt;/li&gt;
&lt;li&gt;Why observability matters more than logging&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That intuition is what separates &lt;em&gt;copy-paste&lt;/em&gt; from &lt;em&gt;craftsmanship&lt;/em&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Practical DevOps Path
&lt;/h2&gt;

&lt;p&gt;Here’s how you should think about your journey if you’re starting &lt;strong&gt;now&lt;/strong&gt;:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Master Linux and Command Line
&lt;/h3&gt;

&lt;p&gt;Everything in DevOps runs on CLI, shells, permissions, networking, logs, and processes. Your laptop is your first “cluster.”&lt;br&gt;
If you can’t diagnose why SSH fails, nothing else will help.&lt;/p&gt;

&lt;p&gt;In 2026, this fundamental still underpins container workloads, cloud instances, observability agents, and automation scripts.&lt;/p&gt;




&lt;h3&gt;
  
  
  2. Scripting &amp;amp; Automation
&lt;/h3&gt;

&lt;p&gt;If you automate only once manually, that’s already DevOps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bash/Python/Go for straightforward automation&lt;/li&gt;
&lt;li&gt;Tools like Make, just to organize tasks&lt;/li&gt;
&lt;li&gt;A mindset of &lt;em&gt;automate early, fix once&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AI can &lt;em&gt;suggest scripts&lt;/em&gt; — but always read and refactor them yourself.&lt;/p&gt;




&lt;h3&gt;
  
  
  3. Containers &amp;amp; Microservices
&lt;/h3&gt;

&lt;p&gt;Docker, Kubernetes, and sidecar architectures aren’t going anywhere.&lt;br&gt;
These technologies form the backbone of cloud-native systems and service distribution.&lt;/p&gt;

&lt;p&gt;Remember:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Docker makes shipping easier; Kubernetes makes scaling easier —&lt;br&gt;
you still need to know &lt;em&gt;what each component does&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  4. Real Cloud Practice (Practically)
&lt;/h3&gt;

&lt;p&gt;Cloud isn’t just AWS, Azure, or GCP; it’s &lt;em&gt;how production systems run&lt;/em&gt;. AI-native cloud services are replacing old workloads, and multi-cloud/hybrid strategies are increasingly popular.&lt;/p&gt;

&lt;p&gt;Instead of incurring huge bills while you learn, consider tools like &lt;strong&gt;LocalStack&lt;/strong&gt; (&lt;a href="https://github.com/localstack/localstack" rel="noopener noreferrer"&gt;https://github.com/localstack/localstack&lt;/a&gt;) — it lets you &lt;em&gt;practice cloud APIs locally&lt;/em&gt; without risk. That’s real hands-on learning.&lt;/p&gt;




&lt;h3&gt;
  
  
  5. CI/CD and Beyond (From Commit to Deployment)
&lt;/h3&gt;

&lt;p&gt;This is where theory intersects practice.&lt;/p&gt;

&lt;p&gt;You won’t just be writing pipelines, you treat them like &lt;strong&gt;policies&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Automated testing&lt;/li&gt;
&lt;li&gt;Security scanning&lt;/li&gt;
&lt;li&gt;Version-controlled provisioning&lt;/li&gt;
&lt;li&gt;Fast rollback strategies&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In 2026, AI isn’t replacing CI/CD engineers, it’s &lt;em&gt;augmenting&lt;/em&gt; them via smart predictions and autonomous decision loops.&lt;/p&gt;




&lt;h3&gt;
  
  
  6. Security &amp;amp; Resilience
&lt;/h3&gt;

&lt;p&gt;DevSecOps is now standard practice:&lt;br&gt;
Security checks must happen &lt;em&gt;before&lt;/em&gt; deployments, not after!&lt;/p&gt;

&lt;p&gt;You need broad fluency with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Secrets management (Vault, AWS Secrets Manager)&lt;/li&gt;
&lt;li&gt;Policy as code (OPA, Kyverno)&lt;/li&gt;
&lt;li&gt;Automated vulnerability scanning&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Security is not a separate discipline; it’s a microbial process embedded throughout your pipelines.&lt;/p&gt;




&lt;h2&gt;
  
  
  How AI Helps (And When It Hurts)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ✔️ Where AI Adds Massive Value
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Generating boilerplate Terraform/CI/CD&lt;/li&gt;
&lt;li&gt;Explaining error logs in clear language&lt;/li&gt;
&lt;li&gt;Offering alternative solutions fast&lt;/li&gt;
&lt;li&gt;Flagging potential configuration problems early&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  ❌ Where AI Can Mislead
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Blindly copying code without understanding it&lt;/li&gt;
&lt;li&gt;Masking architectural tradeoffs&lt;/li&gt;
&lt;li&gt;Assuming generated configs are optimal&lt;/li&gt;
&lt;li&gt;Trusting AI output without testing it thoroughly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb:&lt;/strong&gt;&lt;br&gt;
If AI helps you &lt;em&gt;understand&lt;/em&gt; a solution — it’s good.&lt;br&gt;
If it only helps you &lt;em&gt;run&lt;/em&gt; a solution — it’s dangerous.&lt;/p&gt;




&lt;h2&gt;
  
  
  These Resources Will Accelerate Your Journey
&lt;/h2&gt;

&lt;p&gt;You might find these extremely useful:&lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;100 Days of DevOps&lt;/strong&gt; — a proven, hands-on progression:&lt;br&gt;
&lt;a href="https://github.com/rufilboss/100DaysOfDevOps" rel="noopener noreferrer"&gt;https://github.com/rufilboss/100DaysOfDevOps&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;DevOps Guide (fork &amp;amp; extend it):&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://github.com/rufilboss/DevOps-Guide/" rel="noopener noreferrer"&gt;https://github.com/rufilboss/DevOps-Guide/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;DevOps Books curated by the community:&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://github.com/DevOps-Projects-Ideas/DevOps-Books/" rel="noopener noreferrer"&gt;https://github.com/DevOps-Projects-Ideas/DevOps-Books/&lt;/a&gt;&lt;br&gt;
(500+ stars | 270+ forks — real community validation)&lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;Cloud infrastructure local practice (Low cost/no risk):&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://github.com/localstack/localstack" rel="noopener noreferrer"&gt;https://github.com/localstack/localstack&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;These are not ads, they are &lt;strong&gt;battle-tested resources&lt;/strong&gt; used by beginners and pros alike.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Real Takeaway
&lt;/h2&gt;

&lt;p&gt;DevOps in 2026 is not &lt;em&gt;just another career&lt;/em&gt;.&lt;br&gt;
It’s about &lt;strong&gt;thinking in systems&lt;/strong&gt;, not tools.&lt;/p&gt;

&lt;p&gt;AI will let you &lt;em&gt;explore more&lt;/em&gt;, but only fundamentals will let you &lt;em&gt;master more&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;You don’t need to learn everything today, but you do need to build &lt;strong&gt;confidence through repetition, testing, failure, and curiosity&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you’re ready to dive in, you’re already ahead of the crowd.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>beginners</category>
      <category>ai</category>
      <category>cloud</category>
    </item>
    <item>
      <title>A Practical Guide to Organizing an AWS Community Day (From Scratch)</title>
      <dc:creator>Ilyas Rufai</dc:creator>
      <pubDate>Tue, 23 Dec 2025 11:59:50 +0000</pubDate>
      <link>https://dev.to/rufilboss/a-practical-guide-to-organizing-an-aws-community-day-from-scratch-1ao4</link>
      <guid>https://dev.to/rufilboss/a-practical-guide-to-organizing-an-aws-community-day-from-scratch-1ao4</guid>
      <description>&lt;h2&gt;
  
  
  What is AWS Community Day?
&lt;/h2&gt;

&lt;p&gt;AWS Community Day is a &lt;strong&gt;one-day, community-led conference&lt;/strong&gt; organized by AWS communities such as AWS User Groups or AWS Cloud Clubs. It brings cloud practitioners, students, and enthusiasts together to learn, network, and share real-world AWS knowledge.&lt;/p&gt;

&lt;p&gt;Community Days range from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Large, multi-country events (e.g., AWS Community Day DACH)&lt;/li&gt;
&lt;li&gt;To smaller, single-community events organized by one User Group or Cloud Club&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This guide documents &lt;strong&gt;what it takes to plan, organize, and execute an AWS Community Day successfully&lt;/strong&gt;, especially if you’re doing it for the &lt;strong&gt;first time&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 1: Foundations (Before Anything Else)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Define Your Scope
&lt;/h3&gt;

&lt;p&gt;Before tools, sponsors, or speakers, be clear on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Target audience:&lt;/strong&gt; students, professionals, beginners, mixed?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Event size goal:&lt;/strong&gt; (e.g. 1000–1500 attendees)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Event type:&lt;/strong&gt; free or paid&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Location:&lt;/strong&gt; city, campus, venue type&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;📌 &lt;em&gt;Placeholder:&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Expected attendees: ___&lt;br&gt;
Target audience: ___&lt;br&gt;
City/Campus: ___&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  2. Official AWS Alignment (Very Important)
&lt;/h3&gt;

&lt;h4&gt;
  
  
  AWS Community Day Page
&lt;/h4&gt;

&lt;p&gt;Start here:&lt;br&gt;
👉 &lt;a href="https://aws.amazon.com/developer/community/community-day/" rel="noopener noreferrer"&gt;https://aws.amazon.com/developer/community/community-day/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This page explains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What qualifies as an AWS Community Day&lt;/li&gt;
&lt;li&gt;Branding rules&lt;/li&gt;
&lt;li&gt;FAQs&lt;/li&gt;
&lt;li&gt;Organizer expectations&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  AWS Organizer Slack
&lt;/h4&gt;

&lt;p&gt;Join the &lt;strong&gt;&lt;code&gt;community-day-organizers&lt;/code&gt;&lt;/strong&gt; Slack channel.&lt;br&gt;
This is critical for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Avoiding date clashes in the same region&lt;/li&gt;
&lt;li&gt;Learning from other organizers&lt;/li&gt;
&lt;li&gt;Funding guidance&lt;/li&gt;
&lt;li&gt;Shared templates and experiences&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Phase 2: Online Presence &amp;amp; Registration
&lt;/h2&gt;

&lt;h3&gt;
  
  
  3. Event Website
&lt;/h3&gt;

&lt;p&gt;You will need a simple website containing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Event details&lt;/li&gt;
&lt;li&gt;Agenda&lt;/li&gt;
&lt;li&gt;Speakers&lt;/li&gt;
&lt;li&gt;Registration link&lt;/li&gt;
&lt;li&gt;Sponsors&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Build your own&lt;/li&gt;
&lt;li&gt;Use a template (e.g. Hugo-based AWS Community Day templates)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;📌 &lt;em&gt;Placeholder:&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Website URL: ___&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  4. Registration
&lt;/h3&gt;

&lt;p&gt;Common tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Eventbrite&lt;/li&gt;
&lt;li&gt;Google Forms&lt;/li&gt;
&lt;li&gt;Konfhub&lt;/li&gt;
&lt;li&gt;Other ticketing platforms&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Choose one that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Handles attendee limits&lt;/li&gt;
&lt;li&gt;Exports attendee data&lt;/li&gt;
&lt;li&gt;Supports QR codes (nice bonus)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;📌 &lt;em&gt;Placeholder:&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Registration platform: ___&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  5. Call for Speakers (CfS)
&lt;/h3&gt;

&lt;p&gt;You’ll need speakers early.&lt;/p&gt;

&lt;p&gt;Tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sessionize&lt;/li&gt;
&lt;li&gt;Google Forms&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Collect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Topic title&lt;/li&gt;
&lt;li&gt;Abstract&lt;/li&gt;
&lt;li&gt;Speaker bio&lt;/li&gt;
&lt;li&gt;Preferred time slot&lt;/li&gt;
&lt;li&gt;Session level (Beginner / Intermediate / Advanced)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;📌 &lt;em&gt;Placeholder:&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;CfS link: ___&lt;br&gt;
Submission deadline: ___&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Phase 3: AWS Support &amp;amp; Resources
&lt;/h2&gt;

&lt;h3&gt;
  
  
  6. AWS-Provided Materials
&lt;/h3&gt;

&lt;p&gt;AWS provides downloadable assets that help a lot:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Logos&lt;/li&gt;
&lt;li&gt;Fonts&lt;/li&gt;
&lt;li&gt;Slide templates&lt;/li&gt;
&lt;li&gt;Organizer resources&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Look for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;UG Toolkit / Community Toolkit&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  7. Funding (Yes, It’s Possible 💵)
&lt;/h3&gt;

&lt;p&gt;AWS may provide &lt;strong&gt;financial or material support&lt;/strong&gt; depending on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Event size&lt;/li&gt;
&lt;li&gt;Community maturity&lt;/li&gt;
&lt;li&gt;Region&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Funding requests are usually discussed in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AWS organizer Slack&lt;/li&gt;
&lt;li&gt;Via AWS Community contacts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;📌 &lt;em&gt;Placeholder:&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Funding requested? Yes / No&lt;br&gt;
Amount / type: ___&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Phase 4: Planning the Actual Event
&lt;/h2&gt;

&lt;h3&gt;
  
  
  8. Attendee Estimation (Be Realistic)
&lt;/h3&gt;

&lt;p&gt;Estimating attendance is tricky.&lt;/p&gt;

&lt;p&gt;Consider:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Size of your community&lt;/li&gt;
&lt;li&gt;Average meetup attendance&lt;/li&gt;
&lt;li&gt;Travel distance&lt;/li&gt;
&lt;li&gt;Marketing reach&lt;/li&gt;
&lt;li&gt;Exam periods / holidays&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rule of thumb:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Expect less → Be pleasantly surprised later&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Also expect &lt;strong&gt;last-minute registrations&lt;/strong&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  9. Venue Selection
&lt;/h3&gt;

&lt;p&gt;Choose a venue that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can scale up or down&lt;/li&gt;
&lt;li&gt;Supports multiple rooms&lt;/li&gt;
&lt;li&gt;Has reliable power &amp;amp; internet&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Minimum rooms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Main session room(s)&lt;/li&gt;
&lt;li&gt;Speakers’ room&lt;/li&gt;
&lt;li&gt;Storage / quiet room&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;📌 &lt;em&gt;Tip:&lt;/em&gt;&lt;br&gt;
Sponsors/expo area should be &lt;strong&gt;where attendees naturally pass&lt;/strong&gt;, not isolated.&lt;/p&gt;




&lt;h3&gt;
  
  
  10. Catering
&lt;/h3&gt;

&lt;p&gt;Keep it simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Snacks&lt;/li&gt;
&lt;li&gt;Lunch&lt;/li&gt;
&lt;li&gt;Drinks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Don’t forget:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Speakers’ room refreshments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;📌 &lt;em&gt;Placeholder:&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Catering plan: ___&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  11. Tracks &amp;amp; Agenda Design
&lt;/h3&gt;

&lt;p&gt;Less is more.&lt;/p&gt;

&lt;p&gt;Avoid:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Too many parallel tracks&lt;/li&gt;
&lt;li&gt;Overlapping highly attractive sessions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Recommended format (per session):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;30 min talk&lt;/li&gt;
&lt;li&gt;15 min Q&amp;amp;A&lt;/li&gt;
&lt;li&gt;15 min break / expo&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Sample Agenda
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;08:00 – Registration
09:00 – Opening Remarks
09:15 – Keynote
10:00 – Break / Expo
10:30 – Session Slot 1
11:30 – Session Slot 2
12:30 – Lunch
13:30 – Session Slot 3
14:30 – Session Slot 4
15:30 – Break
16:00 – Final Session
16:30 – Closing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  12. Speakers
&lt;/h3&gt;

&lt;p&gt;Aim for balance:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AWS employees&lt;/li&gt;
&lt;li&gt;Community speakers&lt;/li&gt;
&lt;li&gt;First-time speakers&lt;/li&gt;
&lt;li&gt;Local &amp;amp; external speakers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Always ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Preferred speaking time&lt;/li&gt;
&lt;li&gt;Travel needs&lt;/li&gt;
&lt;li&gt;Slide sharing permission&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  13. Free vs Paid Event
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Free Event&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Higher no-shows&lt;/li&gt;
&lt;li&gt;More inclusive&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Paid Event&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fewer no-shows&lt;/li&gt;
&lt;li&gt;More admin (tax, accounting)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Choose what fits your context.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 5: Marketing &amp;amp; Sponsors
&lt;/h2&gt;

&lt;h3&gt;
  
  
  14. Marketing (Don’t Underestimate This)
&lt;/h3&gt;

&lt;p&gt;Channels to use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;WhatsApp groups&lt;/li&gt;
&lt;li&gt;LinkedIn&lt;/li&gt;
&lt;li&gt;Twitter/X&lt;/li&gt;
&lt;li&gt;Campus clubs&lt;/li&gt;
&lt;li&gt;Word of mouth&lt;/li&gt;
&lt;li&gt;Sponsors’ channels&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Start early.&lt;/p&gt;




&lt;h3&gt;
  
  
  15. Sponsors
&lt;/h3&gt;

&lt;p&gt;Sponsors fund your event.&lt;/p&gt;

&lt;p&gt;Prepare:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Clear sponsorship packages&lt;/li&gt;
&lt;li&gt;Benefits list (booth, logo, talk, etc.)&lt;/li&gt;
&lt;li&gt;Venue layout&lt;/li&gt;
&lt;li&gt;Simple agreement&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some sponsors may qualify for &lt;strong&gt;AWS MDF funding&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;📌 &lt;em&gt;Placeholder:&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Sponsor tiers: ___&lt;br&gt;
Confirmed sponsors: ___&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Phase 6: Operations &amp;amp; Logistics
&lt;/h2&gt;

&lt;h3&gt;
  
  
  16. Organizing Team
&lt;/h3&gt;

&lt;p&gt;Small teams can work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;2–5 core organizers&lt;/li&gt;
&lt;li&gt;Clear roles (logistics, speakers, sponsors, media)&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  17. Volunteers
&lt;/h3&gt;

&lt;p&gt;Volunteers help with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Registration&lt;/li&gt;
&lt;li&gt;Directions&lt;/li&gt;
&lt;li&gt;Speaker support&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tip:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Ask sponsors if they can assign volunteers.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  18. Badges, Lanyards &amp;amp; Printing
&lt;/h3&gt;

&lt;p&gt;Decide:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pre-printed vs on-site printing&lt;/li&gt;
&lt;li&gt;Badge color coding (Organizers, Speakers, Sponsors, Attendees)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Reusable materials save money long-term.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 7: Communication &amp;amp; Experience
&lt;/h2&gt;

&lt;h3&gt;
  
  
  19. Communication Channels
&lt;/h3&gt;

&lt;p&gt;Recommended:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Slack (organizers + speakers)&lt;/li&gt;
&lt;li&gt;WhatsApp (organizers + volunteers)&lt;/li&gt;
&lt;li&gt;WhatsApp (real-time event coordination)&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  20. Speaker Slides &amp;amp; Content
&lt;/h3&gt;

&lt;p&gt;Attendees often ask for slides.&lt;/p&gt;

&lt;p&gt;Before the event:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ask speakers if slides can be shared
After the event:&lt;/li&gt;
&lt;li&gt;Upload to website or shared drive&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  21. Speaker Dinner (Highly Recommended)
&lt;/h3&gt;

&lt;p&gt;If budget allows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Host a speaker dinner&lt;/li&gt;
&lt;li&gt;Builds relationships&lt;/li&gt;
&lt;li&gt;Improves speaker experience&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Reality Check 😅
&lt;/h2&gt;

&lt;p&gt;People will:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Complain&lt;/li&gt;
&lt;li&gt;Ask last-minute questions&lt;/li&gt;
&lt;li&gt;Need help&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is normal. Expect it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Organizing an AWS Community Day is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Hard work&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Time-consuming&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Extremely rewarding&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From idea to execution, expect &lt;strong&gt;months of planning&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you’re thinking about doing it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Yes — do it.&lt;/strong&gt;&lt;br&gt;
The impact on your community is worth it.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>aws</category>
      <category>awscloud</category>
      <category>cloudcomputing</category>
      <category>awscommunityday</category>
    </item>
  </channel>
</rss>
