AWS Lambda's execution model creates a specific security blind spot: every cold start injects short-lived IAM credentials into an environment that third-party code — layers, extensions, and dependencies — can read just as easily as your handler can. In April 2022, Cado Security Labs disclosed Denonia, the first malware built specifically for Lambda, a Go binary that ran a Monero cryptominer inside compromised functions and used DNS-over-HTTPS to evade the network egress monitoring most teams rely on for serverless workloads. That incident wasn't really about Lambda being "hacked" — it was about execution roles carrying more permission than the function needed, and nobody checking. Cold start optimization gets the engineering attention (memory tuning, provisioned concurrency, SnapStart); permission scope gets almost none. This piece walks through what actually happens to credentials during a cold start, where over-permissioned roles come from, and how to catch permission drift before it ships.
What happens to IAM credentials during a Lambda cold start?
On a cold start, Lambda calls AWS STS to assume the function's execution role and injects the resulting temporary credentials — AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN — as environment variables before your handler code executes at all. Those credentials are readable by anything running inside that Firecracker microVM: your handler, any Lambda layer bundled into the deployment package, and any extension registered through the Extensions API, which AWS made generally available in October 2020 specifically so third-party agents (APM, security, logging) could run alongside function code with init-phase visibility. The credentials stay valid and in memory for the lifetime of the sandbox — AWS typically recycles an idle execution environment after roughly 5 to 15 minutes of inactivity, but under sustained invocation the same container, and the same injected credentials, can be reused for hours. Cold start duration itself varies by runtime: a lightweight Node.js function often initializes in 100–300ms, while a Java function without SnapStart can take 1–6 seconds. SnapStart, announced at re:Invent in November 2022 for Java and extended to Python and .NET in 2024, cuts that by up to 90% by resuming from a cached, encrypted snapshot — but the snapshot still contains whatever permissions the role had at publish time.
Why do over-permissioned execution roles increase Lambda's blast radius?
Because most execution roles are built from broad managed policies rather than scoped to the three or four API calls the function actually makes. The AWS-managed AWSLambdaBasicExecutionRole policy, attached by default in AWS SAM and Serverless Framework templates, grants logs:CreateLogGroup, logs:CreateLogStream, and logs:PutLogEvents on Resource: "*" unless a team explicitly scopes it to a single log group ARN. Teams then layer on s3:* or dynamodb:* for convenience during development and never revisit it. Spencer Gietzen's 2019 AWS IAM privilege escalation research (still one of the most cited enumerations of the attack class) documented at least two Lambda-specific escalation paths: a role with iam:PassRole plus lambda:CreateFunction can create a new function bound to an administrator role, and a role with lambda:UpdateFunctionCode on a function that already runs with elevated permissions can rewrite that function's code to do anything the role is allowed to do. Denonia's operators didn't need a Lambda-specific exploit — they needed one function with excess permission and outbound network access.
Can malware persist inside a warm Lambda container between invocations?
Yes, and Denonia is the proof: once it compromised a function, it stayed resident in the warm execution environment across invocations, running its XMRig-based miner in the background while the function continued serving legitimate requests. It used DNS-over-HTTPS through public resolvers to route command-and-control traffic, specifically because Lambda functions inside a VPC rarely have DNS-layer inspection even when their outbound HTTP/S traffic is logged through a NAT gateway or VPC endpoint. The Extensions API compounds this: an extension process registered during the INIT phase can keep running in the background for the container's entire warm lifetime, including through freeze-and-thaw cycles between invocations, which means a malicious or compromised extension doesn't need to re-establish access on every call — it just waits.
How do Lambda layers and third-party dependencies expand the attack surface?
They expand it because every layer and package in the deployment bundle runs in the same process space as your handler and inherits the exact same execution role — there is no permission boundary between your code and a dependency's code. The October 2021 compromise of ua-parser-js, a library pulled into countless Node.js Lambda functions as a transitive dependency, shipped versions 0.7.29, 0.8.1, and 1.0.1 with a payload that installed a cryptominer and a Windows/Linux credential-stealing trojan within hours of publication. A serverless function with that dependency in its bundle would have handed the payload direct access to whatever IAM credentials and environment-variable secrets the execution role carried. Lambda layers make this worse operationally: a layer is an opaque zip up to 250MB unzipped, often built once and shared across a dozen functions and multiple AWS accounts, and it's rarely re-scanned once it's published because nothing in the deploy pipeline treats it as its own artifact with its own SBOM.
What's the fastest way to tell if a function's permissions exceed what its code actually calls?
Reachability analysis — comparing the IAM policy JSON attached to the execution role against the AWS SDK calls that are actually reachable from the handler's code paths, not just what's declared in a package.json or requirements.txt. A function with s3:*, dynamodb:*, sns:Publish, and sqs:SendMessage in its policy but only s3.getObject and dynamodb.getItem in its reachable call graph has roughly 90% of its granted actions sitting unused, and any one of them is a viable path for an attacker who lands code execution through a compromised dependency. This is a fundamentally different question than the one most teams ask during cold start tuning, which is about memory allocation, package size (Lambda deployment packages are capped at 50MB zipped, 250MB unzipped including layers), and init duration — none of which touches whether the role can be abused.
When should teams re-check Lambda functions for permission drift?
On every deploy, not on a quarterly audit — because IAM policy changes ship in the same CI/CD pipeline as code changes and can widen a role silently. A serverless deploy or sam deploy triggered from a GitHub Actions workflow on every merge to main can introduce a broadened Action block through a template merge that nobody reviewed as a security change, because it looked like a routine dependency bump or a new event source mapping. Teams running multiple deploys a day to the same function accumulate permission drift the same way they accumulate dependency drift, and without a check gating the pipeline, the first place anyone notices is an incident report.
How Safeguard Helps
Safeguard maps exactly this gap between granted and used permissions for serverless workloads: reachability analysis traces which IAM actions a Lambda function's code paths can actually invoke and flags every unused grant in the execution role as excess attack surface, not just a policy-linting warning. Griffin AI correlates that reachability data with the function's dependency tree and any known-exploited packages — surfacing cases like a ua-parser-js-style compromise sitting inside a layer with unrestricted s3:* access — and prioritizes them by actual exploitability rather than raw CVE count. Safeguard generates and ingests SBOMs per function and per layer, so shared layers get tracked as first-class artifacts instead of disappearing into opaque zips. When drift is found, Safeguard opens an auto-fix pull request that scopes the IAM policy down to the permissions the reachability graph confirms are in use, so the fix ships through the same review process as the code change that caused it.