cd /news/developer-tools/your-lambda-passes-tests-then-throws… · home topics developer-tools article
[ARTICLE · art-75000] src=dev.to ↗ pub= topic=developer-tools verified=true sentiment=· neutral

Your Lambda Passes Tests Then Throws AccessDeniedException in Production

A developer built Infrawise, an open-source tool that prevents AWS AccessDeniedException errors by comparing a Lambda function's code dependencies against its IAM role permissions. The tool scans code for service edges like DynamoDB queries or SNS publishes, fetches the actual allowed services from the role's policies, and reports mismatches before deployment.

read7 min views1 publishedJul 27, 2026

You asked Claude Code to add a step to your order handler: pull the Stripe key out of Secrets Manager, then publish an order.settled

event to SNS. It wrote clean code. Your unit tests mock both SDK clients, so they pass. CI is green. You deploy.

Then the first real invocation logs this:

AccessDeniedException: User: arn:aws:sts::123456789012:assumed-role/order-processor-role/order-processor
is not authorized to perform: sns:Publish on resource: arn:aws:sns:us-east-1:123456789012:order-events

Nothing was wrong with the code. The execution role was written months ago, when the function only touched DynamoDB. The code grew a new dependency and the policy did not follow.

This is one of the most annoying classes of AWS bug because it is invisible to every tool in the loop. The type checker sees a valid PublishCommand

. The linter sees valid TypeScript. The AI assistant reads your handler and has no idea what order-processor-role

allows, because the policy lives in AWS (or in a Terraform file three directories away), not in the file it is editing.

Infrawise closes that gap by comparing the two directly. GitHub · npm

An AI coding assistant works from the files you give it. When it writes a Lambda handler, it can see:

What it cannot see is the runtime identity that code will execute as. The link between "this handler" and "this IAM role" is a Role

property on the deployed function, resolved through an ARN, and the policy behind that ARN is a set of JSON documents attached to a role name. Three lookups away from anything in your repo.

So the assistant does the only thing it can: it assumes the permissions exist. Ninety percent of the time they do, which is exactly what makes the other ten percent expensive. The failure surfaces at runtime, in an environment where you are reading CloudWatch instead of your editor.

Infrawise already builds a graph of your infrastructure and your code. The AST scan produces typed edges from functions to the things they touch: a query

or scan

edge to a DynamoDB table, a reads_secret

edge to a Secrets Manager secret, a reads_parameter

edge to an SSM parameter, a publishes_to

edge to an SQS queue or an SNS topic.

Separately, the Lambda extractor pulls roleArn

off every function, then fetches what that role permits. It batches the fetch per unique role ARN, so ten functions sharing one role cost one IAM round trip, not ten. For each role it walks both attached managed policies and inline policies, decodes the policy documents, and reduces every Allow

statement to the service prefix of its actions. sns:Publish

becomes sns

. dynamodb:GetItem

becomes dynamodb

. A bare "*"

action becomes *

.

The result is allowedServices

, a flat list like ["logs", "dynamodb"]

.

Now the comparison is trivial. Walk the function's outgoing edges, map each to the service prefix it requires, and subtract allowedServices

. What is left is what the code needs and the role does not grant.

The mapping is deliberately narrow. Five edge types map to five services:

Edge Required service
query or scan on a DynamoDB table
dynamodb
reads_secret
secretsmanager
reads_parameter
ssm
publishes_to a queue
sqs
publishes_to a topic
sns

Two cases produce no finding at all, on purpose:

No IAM data. If the IAM read fails (missing permission, SCP, whatever), allowedServices

comes back undefined and the analyzer skips the function entirely. Infrawise will not tell you a permission is missing when it could not read the policy. Silence beats a false alarm you learn to ignore.

Wildcard. If the role has "Action": "*"

anywhere, the service list contains *

and the function is skipped. AdministratorAccess is its own problem, but it is not a missing-permission problem.

Two places, and they cover the two moments where it matters.

While you are writing the handler. analyze_function

returns missingPermissions

alongside everything else it knows about the function:

{
  "function": "order-processor",
  "found": true,
  "file": "src/handlers/order-processor.ts",
  "triggers": [
    { "type": "sqs", "source": "orders", "eventShape": "event.Records[0].body" }
  ],
  "accesses": [
    { "targetId": "table:dynamo:Orders", "edgeType": "query", "targetName": "Orders", "targetType": "table" },
    { "targetId": "secret:aws:stripe/live", "edgeType": "reads_secret", "targetName": "stripe/live", "targetType": "secret" },
    { "targetId": "topic:aws:order-events", "edgeType": "publishes_to", "targetName": "order-events", "targetType": "topic" }
  ],
  "missingPermissions": ["secretsmanager", "sns"],
  "issues": [ ... ],
  "recommendations": [ ... ]
}

That is the whole point. The assistant asked one question before writing code and got back the trigger event shape and the fact that two of the three services this function talks to are not in its role. It can tell you to fix the policy in the same breath as it writes the handler.

Note the field is omitted entirely when IAM data was unavailable, rather than returned as an empty array. An empty missingPermissions

means "checked, nothing missing." An absent one means "not checked." Those are different answers and conflating them is how you get misplaced confidence.

Before you deploy. The same comparison runs as a high-severity analyzer in infrawise analyze

and infrawise check

:

  1.  HIGH   Lambda "order-processor" accesses sns but execution role has no sns permissions
       "order-processor" calls sns in code but its IAM execution role (arn:aws:iam::123456789012:role/order-processor-role) has no sns:* permissions. This will cause AccessDeniedException at runtime — code passes tests but fails in AWS.
       → Add sns permissions to the execution role for "order-processor". Minimum required: sns:Publish.

The recommendation names actual actions, not sns:*

. Each service has a minimal action set baked in: secretsmanager:GetSecretValue

for secrets, ssm:GetParameter, ssm:GetParameters, ssm:GetParametersByPath

for parameters, sqs:SendMessage, sqs:ReceiveMessage, sqs:DeleteMessage, sqs:GetQueueAttributes

for queues, and the CRUD-plus-query set for DynamoDB. You can paste the list into the policy without thinking about whether you just granted dynamodb:DeleteTable

.

Because it is a high-severity finding, infrawise check

fails the build on it by default. That is the whole CI story: check

runs a fresh analysis (never a cached graph, since CI gating on stale data is worse than no gating) and exits non-zero when anything at or above --fail-on

shows up. Default threshold is high

.

infrawise check

Names have to line up. The check joins a code function node to a Lambda node by exact name match. If your handler is exported as handler

in src/orders.ts

but deployed as order-processor

, infrawise finds no code node for that Lambda and produces no finding. It fails closed, which is the right direction, but it means the check is only as good as your naming discipline. If you use a framework that derives function names from the deployed name, you get this for free.

Granularity stops at the service prefix. Infrawise compares sns

against sns

, not arn:aws:sns:us-east-1:123456789012:order-events

against the resource in your PublishCommand

. A role scoped to one topic passes the check even if your code publishes to a different topic. So this catches "the policy has no SNS at all," which is the common case, not "the policy has the wrong SNS resource." Resource-level checking is a different problem and pretending otherwise would make the finding untrustworthy.

It needs read access to IAM. Five actions: iam:ListAttachedRolePolicies

, iam:ListRolePolicies

, iam:GetRolePolicy

, iam:GetPolicy

, iam:GetPolicyVersion

. They are optional in the infrawise IAM policy. Drop them and everything else still works, you just lose this check. Infrawise is read-only across the board, so nothing here writes to your account.

cd your-project
npx infrawise start --claude

That probes your environment, writes infrawise.yaml

and .mcp.json

, and opens Claude Code with all 21 tools connected. Ask it to write a Lambda that reads a secret, and watch it check the role before it writes the code.

AccessDeniedException

in a Lambda is almost never a code bug. It is a drift bug: the code grew a dependency, the execution role did not follow.analyze_function

before writing a handler to get missingPermissions

alongside the trigger event shape. An absent field means IAM was not readable; an empty array means it was checked and clean.infrawise check

in CI. Missing permissions are high severity, so the default --fail-on high

blocks the deploy.

── more in #developer-tools 4 stories · sorted by recency
── more on @infrawise 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/your-lambda-passes-t…] indexed:0 read:7min 2026-07-27 ·