Opening the Black Box
When you spin up a Lambda function from the console, AWS automatically attaches a
service-role/AWSLambdaBasicExecutionRole policy to the function’s execution
role. This convenience is attractive, but it also creates a hidden attack surface.
The role grants permissions that are often broader than the function truly needs,
and because the role is “default”, developers rarely question its scope.
What the Default Role Actually Gives You
By default the role includes the following managed policies:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:*:*:*"
}
]
}
While logging is essential, many teams add the AWSLambdaVPCAccessExecutionRole
or even AmazonS3FullAccess manually, often without a clear justification.
The result is a role that can read or write to any bucket in the account, enumerate
VPC resources, or even invoke other Lambda functions. In a compromised function,
these privileges become a stepping stone for lateral movement.
Step‑by‑Step Auditing with the AWS CLI
Before you refactor, you need to know exactly what the role can do. The following Bash snippet enumerates the inline and attached policies of a given Lambda role and prints a concise summary:
#!/usr/bin/env bash
ROLE_NAME=$1
if [[ -z "$ROLE_NAME" ]]; then
echo "Usage: $0 <lambda-exec-role-name>"
exit 1
fi
echo "Fetching attached managed policies..."
aws iam list-attached-role-policies --role-name "$ROLE_NAME" \
--query 'AttachedPolicies[].PolicyArn' --output text
echo "Fetching inline policies..."
aws iam list-role-policies --role-name "$ROLE_NAME" \
--query 'PolicyNames[]' --output text | while read -r POL; do
echo "Inline policy: $POL"
aws iam get-role-policy --role-name "$ROLE_NAME" --policy-name "$POL" \
--query 'PolicyDocument' --output json
done
Run the script with the role name you see in the Lambda console:
./audit_lambda_role.sh myLambdaExecutionRole
The output will reveal any attached AmazonS3FullAccess or similar broad
policies that should be removed. If you spot a policy you don’t recognize, that’s a
red flag.
Automated Least‑Privilege Generation with Python
Once you know the current permissions, you can generate a trimmed policy using
Permissions‑Guard or a
lightweight Python script that calls simulate-principal-policy. The script
below builds a minimal policy that only allows the actions observed during a test run:
import boto3
import json
import time
lambda_name = "my-function"
region = "us-east-1"
# 1. Capture CloudTrail events for the function for the last hour
ct = boto3.client('cloudtrail', region_name=region)
now = int(time.time())
events = ct.lookup_events(
LookupAttributes=[{'AttributeKey': 'ResourceName', 'AttributeValue': lambda_name}],
StartTime=time.gmtime(now - 3600),
EndTime=time.gmtime(now)
)
actions = set()
for e in events['Events']:
details = json.loads(e['CloudTrailEvent'])
actions.add(details['eventName'])
print("Observed actions:", actions)
# 2. Build a policy document granting only those actions on the function
policy = {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": list(actions),
"Resource": f"arn:aws:lambda:{region}:{{account_id}}:function:{lambda_name}"
},
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:*:*:*"
}
]
}
print(json.dumps(policy, indent=2))
Replace {{account_id}} with your AWS account ID and apply the policy
using the CLI:
aws iam put-role-policy --role-name myLambdaExecutionRole \
--policy-name LeastPrivilegePolicy --policy-document file://least_privilege.json
This approach guarantees that the role only possesses permissions that have been observed in production, dramatically shrinking the attack surface.
Security and Best Practices
Never ship a Lambda with the default role unchanged. Treat the role as
code: version it, review it in pull requests, and enforce automated checks with
tools like cfn‑nag or tfsec. Additionally, enable
IAM Access Analyzer
to surface unintended resource access.
Rotate the role’s trust policy regularly and limit the sts:AssumeRole
principal to the exact Lambda service principal and specific function ARN.
Example trust policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {
"aws:SourceArn": "arn:aws:lambda:us-east-1:{{account_id}}:function:my-function"
}
}
}
]
}
By tightening the trust relationship you prevent other Lambda functions or services from assuming the role, a common mis‑configuration that attackers exploit.
“A role that grants more than it needs is not a convenience; it’s a liability waiting for the next exploit.” – AWS Security Blog, 2025
Conclusion
The default Lambda execution role is a classic example of “set‑and‑forget” security. Its convenience masks a silent threat that can be leveraged for privilege escalation, data exfiltration, or lateral movement across your AWS environment. By systematically auditing, generating least‑privilege policies, and hardening the trust relationship, you convert a hidden liability into a controlled, auditable component of your serverless architecture.
Treat every IAM role as a piece of production code: review it, test it, and version‑control it. When you do, the convenience of the default role no longer outweighs the risk, and your serverless workloads stay both agile and secure.