Back to blog
AWSBest PracticesCloud SecurityIdentity & AccessServerless

Lambda Has Admin Privileges: Why It's Dangerous and How to Fix It

AWS Lambda execution roles with wildcard admin permissions turn a single function bug into account takeover. Learn how to detect, fix, and prevent it.

TL;DR

This check flags Lambda execution roles that carry wildcard admin permissions (like "Action": "*" on "Resource": "*"). A compromised function with that role hands an attacker your entire account. Fix it by replacing the broad policy with a scoped one that grants only the actions the function actually uses.

Lambda functions are convenient precisely because they run without you managing servers. But every function assumes an IAM execution role, and that role decides what the function can touch in your account. When that role has administrative privileges, the blast radius of a single bug or dependency compromise goes from "this one function misbehaves" to "an attacker controls the account."

This check looks at the IAM policies attached to your Lambda execution roles and raises a finding when it sees full administrative access granted through wildcards.


What this check detects

The check inspects the execution role assigned to each Lambda function and evaluates the attached and inline policies. It flags any role whose effective permissions include unrestricted admin access. In practice that usually means one of these:

  • The AWS managed AdministratorAccess policy is attached to the role.
  • An inline or customer-managed policy grants "Action": "*" on "Resource": "*" with "Effect": "Allow".
  • A policy grants service-wide wildcards such as "iam:*" or "*:*" that amount to administrative control.

A function does not need to use those permissions for the check to fire. The risk is the granted capability, not the current behavior.

Note: A Lambda execution role is the IAM role Lambda assumes when it runs your function. It is different from the resource-based policy that controls who can invoke the function. This check is about what the running code can do once it executes, not who can trigger it.


Why it matters

Lambda functions sit at the intersection of your application code and your AWS account. They frequently run code pulled from public package registries, parse untrusted input from API Gateway or S3 events, and connect to third-party services. Each of those is a path to compromise.

Picture a function that resizes images uploaded to an S3 bucket. It needs to read from one bucket and write to another. If its execution role carries AdministratorAccess and an attacker finds a vulnerability in the image-processing library, they do not just get the ability to mess with two buckets. They can:

  • Create new IAM users and access keys to establish persistence.
  • Read every secret in Secrets Manager and Parameter Store.
  • Spin up expensive compute for cryptomining.
  • Delete CloudTrail logs to cover their tracks.
  • Snapshot and exfiltrate RDS databases.

This is not theoretical. Server-side request forgery and dependency confusion attacks against serverless functions have repeatedly been used to extract the temporary credentials Lambda injects into the runtime environment. Those credentials inherit the execution role's permissions. An over-privileged role turns a contained incident into a full account takeover.

Warning: Lambda credentials are exposed to the function as environment variables and via the runtime credentials endpoint. Any code execution flaw, including one in a transitive dependency you did not choose, can leak them. Assume the role's permissions are reachable by anything running in the function.


How to fix it

The fix is to replace the wildcard policy with a least-privilege policy scoped to exactly what the function needs. Work through it in three stages: figure out what the function actually uses, write a tight policy, then swap it in.

1. Identify what the function needs

Find the execution role attached to the function:

aws lambda get-function-configuration \
  --function-name image-resizer \
  --query 'Role' \
  --output text

If you do not already know which API calls the function makes, IAM Access Analyzer can generate a policy from the role's CloudTrail history. This is the most reliable way to right-size an existing role.

aws accessanalyzer start-policy-generation \
  --policy-generation-details '{"principalArn":"arn:aws:iam::123456789012:role/image-resizer-role"}' \
  --cloud-trail-details '{
    "trails":[{"cloudTrailArn":"arn:aws:cloudtrail:us-east-1:123456789012:trail/main","allRegions":true}],
    "accessRole":"arn:aws:iam::123456789012:role/AccessAnalyzerCloudTrailRole",
    "startTime":"2024-01-01T00:00:00Z"
  }'

Tip: Access Analyzer needs at least a few weeks of CloudTrail history covering the function's full range of behavior, including infrequent code paths like error handling or monthly batch jobs. Generate the policy after the function has exercised its normal patterns, not the day after deployment.

2. Write a scoped policy

For our image-resizer example, the real requirement is reading from one bucket, writing to another, and emitting logs. That policy looks like this:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ReadSource",
      "Effect": "Allow",
      "Action": ["s3:GetObject"],
      "Resource": "arn:aws:s3:::uploads-bucket/*"
    },
    {
      "Sid": "WriteThumbnails",
      "Effect": "Allow",
      "Action": ["s3:PutObject"],
      "Resource": "arn:aws:s3:::thumbnails-bucket/*"
    },
    {
      "Sid": "WriteLogs",
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Resource": "arn:aws:logs:us-east-1:123456789012:log-group:/aws/lambda/image-resizer:*"
    }
  ]
}

Notice that resources are pinned to specific ARNs, not *. The function can write objects to the thumbnails bucket and nowhere else.

3. Swap the policy in

First detach the admin policy, then attach the scoped one.

Danger: Detaching permissions from a live function's role takes effect immediately. If your scoped policy is missing an action the function genuinely needs, invocations will start failing with AccessDenied. Test in a non-production environment first, and watch the function's error rate during the change.

# Remove the admin policy
aws iam detach-role-policy \
  --role-name image-resizer-role \
  --policy-arn arn:aws:iam::aws:policy/AdministratorAccess

# Create and attach the scoped policy
aws iam create-policy \
  --policy-name image-resizer-least-privilege \
  --policy-document file://image-resizer-policy.json

aws iam attach-role-policy \
  --role-name image-resizer-role \
  --policy-arn arn:aws:iam::123456789012:policy/image-resizer-least-privilege

If the over-privileged grant was an inline policy rather than an attached managed policy, remove it with:

aws iam delete-role-policy \
  --role-name image-resizer-role \
  --policy-name inline-admin-policy

Defining it right in infrastructure as code

If you manage functions with IaC, fix the source of truth so the next deploy does not reintroduce the problem. Here is the least-privilege pattern in Terraform:

resource "aws_iam_role" "image_resizer" {
  name = "image-resizer-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect    = "Allow"
      Principal = { Service = "lambda.amazonaws.com" }
      Action    = "sts:AssumeRole"
    }]
  })
}

resource "aws_iam_role_policy" "image_resizer" {
  name = "image-resizer-least-privilege"
  role = aws_iam_role.image_resizer.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect   = "Allow"
        Action   = ["s3:GetObject"]
        Resource = "${aws_s3_bucket.uploads.arn}/*"
      },
      {
        Effect   = "Allow"
        Action   = ["s3:PutObject"]
        Resource = "${aws_s3_bucket.thumbnails.arn}/*"
      },
      {
        Effect = "Allow"
        Action = ["logs:CreateLogStream", "logs:PutLogEvents"]
        Resource = "${aws_cloudwatch_log_group.image_resizer.arn}:*"
      }
    ]
  })
}

Note: The AWS managed AWSLambdaBasicExecutionRole policy is a reasonable starting point because it grants only CloudWatch Logs permissions. Attach it for logging, then add narrowly scoped statements for everything else the function does.


How to prevent it from happening again

Fixing one role is housekeeping. Stopping admin roles from being created is the actual win. Put a gate between "someone wrote a policy" and "it reached production."

Catch it in CI with policy-as-code

Tools like Checkov, tfsec, or OPA/Conftest can fail a pipeline when a Terraform plan contains a wildcard admin grant attached to a Lambda role. A Conftest rule looks like this:

package main

deny[msg] {
  resource := input.resource.aws_iam_role_policy[name]
  policy := json.unmarshal(resource.policy)
  statement := policy.Statement[_]
  statement.Effect == "Allow"
  statement.Action == "*"
  statement.Resource == "*"
  msg := sprintf("IAM role policy '%s' grants wildcard admin access", [name])
}

Wire that into your pull request checks so a policy with Action: "*" never merges without an explicit, reviewed exception.

Use service control policies as a backstop

For organizations using AWS Organizations, an SCP can prevent execution roles from being granted dangerous permissions in the first place, or block functions from performing sensitive actions like creating IAM users. SCPs apply regardless of what an individual account's IAM policies say, which makes them a strong second line of defense.

Run continuous detection

Lensix runs the lambda_adminprivileges check across your accounts on a schedule so a role that drifts back to admin, whether by a manual console change or a careless deploy, surfaces quickly rather than sitting unnoticed until an incident.

Tip: IAM Access Analyzer also offers unused access findings that flag permissions a role has but has not used in a set period. Reviewing these monthly keeps roles tight over time as functions evolve and old code paths get removed.


Best practices

  • One role per function. Avoid sharing a single execution role across many functions. Shared roles accumulate permissions until they effectively become admin roles by accretion.
  • Pin resources, not just actions. Scoping Action but leaving Resource: "*" still lets a function touch every bucket or table in the account. Constrain both.
  • Never use iam:* in a Lambda role unless the function's entire job is identity management, and even then scope it hard. The ability to create roles and attach policies is the path to privilege escalation.
  • Prefer condition keys for extra fences. Conditions like aws:SourceArn or tag-based access control further limit what a leaked credential can reach.
  • Review on a cadence. Functions change, requirements shrink, and dead code paths leave behind permissions nobody needs. Treat IAM cleanup as routine maintenance, not a one-time project.

The goal is simple to state and worth repeating: a compromised function should be able to damage only what it was built to touch. Least privilege on the execution role is what makes that true.

Fix AWS Lambda Admin Privileges | Lensix | Lensix