cloud security

Securing AWS Lambda Execution Roles

Securing AWS Lambda execution roles by implementing least-privilege IAM policies, applying permission boundaries, restricting resource-based policies, using IAM Access Analyzer to validate permissions, and enforcing role scoping through SCPs.

awscloud-securityexecution-rolesiamlambdaleast-privilege
Install this skill
npx skills add mukul975/Anthropic-Cybersecurity-Skills
Framework mappings

When to Use

  • When deploying new Lambda functions and defining their IAM execution roles
  • When remediating overly permissive Lambda roles discovered during security audits
  • When implementing least-privilege access patterns for serverless architectures
  • When building reusable IAM templates for Lambda functions across teams
  • When Security Hub or Prowler reports Lambda functions with excessive permissions

Do not use for securing Lambda function invocation (use resource-based policies and API Gateway authorizers), for Lambda code security (use SAST tools), or for Lambda network security (use VPC configuration and security groups).

Prerequisites

  • IAM permissions for policy creation, role modification, and Access Analyzer operations
  • AWS IAM Access Analyzer enabled in the account
  • CloudTrail data events enabled for Lambda to capture actual API usage
  • Existing Lambda functions to audit and scope permissions for
  • Understanding of each function's required AWS service interactions

Workflow

Step 1: Audit Current Lambda Execution Role Permissions

Enumerate all Lambda functions and their associated IAM roles to identify over-privileged functions.

# List all Lambda functions with their execution roles
aws lambda list-functions \
  --query 'Functions[*].[FunctionName,Role]' --output table
 
# For each function, analyze attached policies
for func in $(aws lambda list-functions --query 'Functions[*].FunctionName' --output text); do
  role_arn=$(aws lambda get-function-configuration --function-name "$func" --query 'Role' --output text)
  role_name=$(echo "$role_arn" | awk -F'/' '{print $NF}')
  echo "=== $func -> $role_name ==="
 
  # Check for AWS managed policies (often too broad)
  aws iam list-attached-role-policies --role-name "$role_name" \
    --query 'AttachedPolicies[*].[PolicyName,PolicyArn]' --output table
 
  # Check inline policies
  for policy in $(aws iam list-role-policies --role-name "$role_name" --query 'PolicyNames' --output text); do
    echo "  Inline: $policy"
    aws iam get-role-policy --role-name "$role_name" --policy-name "$policy" \
      --query 'PolicyDocument' --output json
  done
done

Step 2: Analyze Actual API Usage with CloudTrail

Use CloudTrail and IAM Access Analyzer to determine which API actions the function actually uses.

# Query CloudTrail for actual API calls made by a Lambda execution role
aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=Username,AttributeValue=LAMBDA_ROLE_NAME \
  --start-time 2026-01-23T00:00:00Z \
  --end-time 2026-02-23T00:00:00Z \
  --query 'Events[*].[EventTime,EventName,EventSource]' \
  --output table | sort -k2 | uniq -f1
 
# Use IAM Access Analyzer policy generation (based on CloudTrail activity)
aws accessanalyzer start-policy-generation \
  --policy-generation-details '{
    "principalArn": "arn:aws:iam::ACCOUNT:role/lambda-execution-role",
    "cloudTrailDetails": {
      "trailArn": "arn:aws:cloudtrail:us-east-1:ACCOUNT:trail/management-trail",
      "startTime": "2026-01-23T00:00:00Z",
      "endTime": "2026-02-23T00:00:00Z"
    }
  }'
 
# Check the generated policy
aws accessanalyzer get-generated-policy \
  --job-id JOB_ID \
  --query 'generatedPolicyResult.generatedPolicies[*].policy'

Step 3: Create Least-Privilege Execution Policies

Build scoped IAM policies that grant only the specific actions and resources each function needs.

# Example: Scoped policy for a function that reads from S3 and writes to DynamoDB
cat > lambda-scoped-policy.json << 'EOF'
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ReadInputBucket",
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:ListBucket"
      ],
      "Resource": [
        "arn:aws:s3:::input-data-bucket",
        "arn:aws:s3:::input-data-bucket/*"
      ]
    },
    {
      "Sid": "WriteDynamoDB",
      "Effect": "Allow",
      "Action": [
        "dynamodb:PutItem",
        "dynamodb:UpdateItem",
        "dynamodb:BatchWriteItem"
      ],
      "Resource": "arn:aws:dynamodb:us-east-1:ACCOUNT:table/results-table"
    },
    {
      "Sid": "CloudWatchLogs",
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Resource": "arn:aws:logs:us-east-1:ACCOUNT:log-group:/aws/lambda/my-function:*"
    }
  ]
}
EOF
 
# Create the policy
aws iam create-policy \
  --policy-name lambda-my-function-policy \
  --policy-document file://lambda-scoped-policy.json
 
# Create execution role with scoped trust policy
cat > lambda-trust-policy.json << 'EOF'
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": {"Service": "lambda.amazonaws.com"},
    "Action": "sts:AssumeRole",
    "Condition": {
      "StringEquals": {
        "aws:SourceAccount": "ACCOUNT_ID"
      }
    }
  }]
}
EOF
 
aws iam create-role \
  --role-name lambda-my-function-role \
  --assume-role-policy-document file://lambda-trust-policy.json
 
aws iam attach-role-policy \
  --role-name lambda-my-function-role \
  --policy-arn arn:aws:iam::ACCOUNT:policy/lambda-my-function-policy

Step 4: Apply Permission Boundaries

Implement permission boundaries to set maximum permissions for Lambda execution roles.

# Create a permission boundary that caps Lambda role capabilities
cat > lambda-permission-boundary.json << 'EOF'
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowedServices",
      "Effect": "Allow",
      "Action": [
        "s3:GetObject", "s3:PutObject", "s3:ListBucket",
        "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:Query", "dynamodb:UpdateItem",
        "sqs:SendMessage", "sqs:ReceiveMessage", "sqs:DeleteMessage",
        "sns:Publish",
        "secretsmanager:GetSecretValue",
        "kms:Decrypt", "kms:GenerateDataKey",
        "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents",
        "xray:PutTraceSegments", "xray:PutTelemetryRecords"
      ],
      "Resource": "*"
    },
    {
      "Sid": "DenyPrivilegeEscalation",
      "Effect": "Deny",
      "Action": [
        "iam:CreateUser", "iam:CreateRole", "iam:CreatePolicy",
        "iam:AttachRolePolicy", "iam:AttachUserPolicy",
        "iam:PutRolePolicy", "iam:PutUserPolicy",
        "iam:CreateAccessKey", "iam:PassRole",
        "lambda:CreateFunction", "lambda:UpdateFunctionConfiguration",
        "sts:AssumeRole"
      ],
      "Resource": "*"
    }
  ]
}
EOF
 
# Create and apply the boundary
aws iam create-policy \
  --policy-name lambda-permission-boundary \
  --policy-document file://lambda-permission-boundary.json
 
aws iam put-role-permissions-boundary \
  --role-name lambda-my-function-role \
  --permissions-boundary arn:aws:iam::ACCOUNT:policy/lambda-permission-boundary

Step 5: Validate Policies with IAM Access Analyzer

Use Access Analyzer to validate policies for security best practices.

# Validate the scoped policy
aws accessanalyzer validate-policy \
  --policy-document file://lambda-scoped-policy.json \
  --policy-type IDENTITY_POLICY \
  --query 'findings[*].[findingType,issueCode,learnMoreLink]' --output table
 
# Check for unused access
aws accessanalyzer check-no-new-access \
  --new-policy-document file://lambda-scoped-policy.json \
  --existing-policy-document file://old-broad-policy.json \
  --policy-type IDENTITY_POLICY
 
# Verify the permission boundary effectiveness
aws iam simulate-principal-policy \
  --policy-source-arn arn:aws:iam::ACCOUNT:role/lambda-my-function-role \
  --action-names iam:CreateUser iam:PassRole s3:GetObject dynamodb:PutItem \
  --query 'EvaluationResults[*].[EvalActionName,EvalDecision]' --output table

Step 6: Enforce Role Standards with SCPs

Apply Service Control Policies to prevent Lambda functions from using overly broad roles.

# SCP to deny Lambda functions using AdministratorAccess
cat > scp-deny-lambda-admin.json << 'EOF'
{
  "Version": "2012-10-17",
  "Statement": [{
    "Sid": "DenyLambdaAdminRole",
    "Effect": "Deny",
    "Action": "lambda:CreateFunction",
    "Resource": "*",
    "Condition": {
      "ForAnyValue:StringLike": {
        "lambda:FunctionArn": "*"
      },
      "ArnLike": {
        "iam:PassedToService": "lambda.amazonaws.com"
      }
    }
  },
  {
    "Sid": "RequirePermissionBoundary",
    "Effect": "Deny",
    "Action": [
      "iam:CreateRole",
      "iam:AttachRolePolicy",
      "iam:PutRolePolicy"
    ],
    "Resource": "arn:aws:iam::*:role/lambda-*",
    "Condition": {
      "StringNotEquals": {
        "iam:PermissionsBoundary": "arn:aws:iam::*:policy/lambda-permission-boundary"
      }
    }
  }]
}
EOF
 
aws organizations create-policy \
  --name "lambda-role-guardrails" \
  --type SERVICE_CONTROL_POLICY \
  --content file://scp-deny-lambda-admin.json

Key Concepts

Term Definition
Execution Role IAM role assumed by Lambda during function execution that defines all AWS API actions the function can perform
Least Privilege Security principle of granting only the minimum permissions required for a function to perform its intended operations
Permission Boundary IAM policy that sets the maximum permissions an execution role can have, even if identity policies grant broader access
IAM Access Analyzer AWS service that generates least-privilege policies based on actual CloudTrail usage and validates policies for security issues
Resource-Scoped Policy IAM policy that specifies exact resource ARNs rather than wildcards, limiting access to only the specific resources needed
Confused Deputy Prevention Adding aws:SourceAccount or aws:SourceArn conditions to trust policies to prevent cross-account role assumption attacks

Tools & Systems

  • IAM Access Analyzer: Generates least-privilege policies from CloudTrail data and validates policy security
  • IAM Policy Simulator: Tests effective permissions for a role against specific API actions before deployment
  • CloudTrail: Audit log of all API calls used to determine actual function permission usage
  • Prowler: Security tool with Lambda-specific checks for role permissions and configuration
  • Checkov: Infrastructure-as-code scanner that validates Lambda IAM policies in CloudFormation/Terraform

Common Scenarios

Scenario: Reducing a Lambda Function from AdministratorAccess to Least Privilege

Context: A security audit finds 12 Lambda functions using a shared execution role with AdministratorAccess. The team needs to scope each function to minimum required permissions without breaking production.

Approach:

  1. Enable CloudTrail data events for Lambda to capture actual API usage per function
  2. Wait 30 days to collect a representative sample of API calls
  3. Use IAM Access Analyzer policy generation for each function's role usage
  4. Create individual scoped policies for each function based on actual API usage
  5. Apply permission boundaries to cap maximum permissions
  6. Deploy scoped roles to staging and run integration tests
  7. Roll out to production with canary deployment and rollback plan
  8. Validate with IAM Policy Simulator before removing the old broad role

Pitfalls: Some Lambda functions may have infrequent code paths that only trigger monthly (batch jobs, error handlers). A 30-day observation window may miss rare API calls. Review the function code alongside CloudTrail data to identify all potential API calls. Use Access Analyzer's policy validation rather than relying solely on generated policies.

Output Format

Lambda Execution Role Security Report
========================================
Account: 123456789012
Review Date: 2026-02-23
Functions Audited: 34
 
ROLE PERMISSION SUMMARY:
  Functions with AdministratorAccess:    3 (CRITICAL)
  Functions with PowerUserAccess:        5 (HIGH)
  Functions with wildcard actions:      12 (MEDIUM)
  Functions with scoped policies:       14 (OK)
 
REMEDIATION PROGRESS:
  [x] payment-processor: Scoped to DynamoDB + S3 + KMS (3 actions)
  [x] order-notification: Scoped to SNS + SES (2 actions)
  [ ] data-pipeline: Generating policy from 30-day CloudTrail data
  [ ] image-resizer: Awaiting staging validation
 
PERMISSION BOUNDARY STATUS:
  Functions with boundary applied:  14 / 34
  Functions without boundary:       20 / 34
 
POLICY VALIDATION RESULTS:
  Policies with security warnings:   4
  Policies with errors:              0
  Policies with suggestions:        12
Source materials

References and resources

Everything below is rendered for inspection. Script files are read-only and never run.

References 1

api-reference.md2.3 KB

API Reference: Securing AWS Lambda Execution Roles

boto3 Lambda Client

Key Methods

Method Description
list_functions() List all Lambda functions with role ARNs and runtime info
get_function_configuration() Get function config including execution role
update_function_configuration() Update function settings (role, KMS key, logging)
create_function_url_config() Configure function URL with auth type

boto3 IAM Client (Role Analysis)

Method Description
get_role() Get role details including trust policy and permission boundary
list_attached_role_policies() List managed policies on a role
list_role_policies() List inline policy names
get_role_policy() Get inline policy document
put_role_permissions_boundary() Apply permission boundary
simulate_principal_policy() Test effective permissions
create_role() Create new role with trust policy
attach_role_policy() Attach a managed policy to a role

boto3 Access Analyzer Client

Method Description
validate_policy() Validate policy against security best practices
start_policy_generation() Generate least-privilege policy from CloudTrail
get_generated_policy() Retrieve generated policy result
check_no_new_access() Verify policy does not grant new access

Trust Policy Structure

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": {"Service": "lambda.amazonaws.com"},
    "Action": "sts:AssumeRole",
    "Condition": {
      "StringEquals": {"aws:SourceAccount": "ACCOUNT_ID"}
    }
  }]
}

Permission Boundary Effect

The effective permissions are the intersection of:

  1. Identity-based policy (attached to role)
  2. Permission boundary (maximum allowed permissions)
  3. Service Control Policies (organizational guardrails)

References

Scripts 1

agent.py7.1 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
"""Agent for auditing and securing AWS Lambda execution roles."""

import boto3
import json
import argparse


def list_lambda_roles(region="us-east-1"):
    """List all Lambda functions and their execution roles."""
    lam = boto3.client("lambda", region_name=region)
    iam = boto3.client("iam")
    functions = []
    paginator = lam.get_paginator("list_functions")
    for page in paginator.paginate():
        for func in page["Functions"]:
            role_arn = func["Role"]
            role_name = role_arn.split("/")[-1]
            functions.append({
                "function_name": func["FunctionName"],
                "runtime": func.get("Runtime", "N/A"),
                "role_name": role_name,
                "role_arn": role_arn,
            })
            print(f"  {func['FunctionName']} -> {role_name} ({func.get('Runtime', 'N/A')})")
    print(f"\n[*] Total Lambda functions: {len(functions)}")
    return functions


def audit_role_permissions(role_name):
    """Analyze attached and inline policies for a Lambda execution role."""
    iam = boto3.client("iam")
    findings = []
    attached = iam.list_attached_role_policies(RoleName=role_name)["AttachedPolicies"]
    broad_policies = ["AdministratorAccess", "PowerUserAccess", "AmazonS3FullAccess",
                      "AmazonDynamoDBFullAccess", "AmazonSQSFullAccess"]
    for pol in attached:
        if pol["PolicyName"] in broad_policies:
            findings.append({
                "type": "OVERPRIVILEGED_MANAGED_POLICY", "severity": "CRITICAL",
                "role": role_name, "policy": pol["PolicyName"],
                "detail": f"Broad managed policy '{pol['PolicyName']}' attached to Lambda role",
            })
            print(f"  [!] CRITICAL: {role_name} has {pol['PolicyName']}")

    inline_names = iam.list_role_policies(RoleName=role_name)["PolicyNames"]
    for pol_name in inline_names:
        doc = iam.get_role_policy(RoleName=role_name, PolicyName=pol_name)["PolicyDocument"]
        for stmt in doc.get("Statement", []):
            if stmt.get("Effect") != "Allow":
                continue
            actions = stmt.get("Action", [])
            resources = stmt.get("Resource", [])
            if isinstance(actions, str):
                actions = [actions]
            if isinstance(resources, str):
                resources = [resources]
            wildcard_actions = [a for a in actions if a.endswith(":*") or a == "*"]
            if wildcard_actions and "*" in resources:
                findings.append({
                    "type": "WILDCARD_POLICY", "severity": "HIGH",
                    "role": role_name, "policy": pol_name,
                    "detail": f"Wildcard actions {wildcard_actions} on Resource '*'",
                })
                print(f"  [!] HIGH: {role_name}/{pol_name} has wildcard actions on *")
    return findings


def check_permission_boundary(role_name):
    """Check if a Lambda execution role has a permission boundary."""
    iam = boto3.client("iam")
    role = iam.get_role(RoleName=role_name)["Role"]
    boundary = role.get("PermissionsBoundary")
    if boundary:
        print(f"  [OK] {role_name} has boundary: {boundary['PermissionsBoundaryArn']}")
        return True
    else:
        print(f"  [!] {role_name} has NO permission boundary")
        return False


def check_trust_policy(role_name):
    """Validate the trust policy for confused deputy prevention."""
    iam = boto3.client("iam")
    role = iam.get_role(RoleName=role_name)["Role"]
    trust_doc = role["AssumeRolePolicyDocument"]
    findings = []
    for stmt in trust_doc.get("Statement", []):
        conditions = stmt.get("Condition", {})
        principal = stmt.get("Principal", {})
        service = principal.get("Service", "")
        if service == "lambda.amazonaws.com" and not conditions:
            findings.append({
                "type": "MISSING_TRUST_CONDITION", "severity": "MEDIUM",
                "role": role_name,
                "detail": "Trust policy lacks aws:SourceAccount or aws:SourceArn condition",
            })
            print(f"  [!] MEDIUM: {role_name} trust policy lacks confused deputy prevention")
    return findings


def validate_with_access_analyzer(policy_document, region="us-east-1"):
    """Validate a policy document using IAM Access Analyzer."""
    aa = boto3.client("accessanalyzer", region_name=region)
    response = aa.validate_policy(
        policyDocument=json.dumps(policy_document),
        policyType="IDENTITY_POLICY",
    )
    findings = response.get("findings", [])
    for f in findings:
        print(f"  [{f['findingType']}] {f['issueCode']}: {f.get('findingDetails', '')}")
    return findings


def full_audit(region="us-east-1"):
    """Run a complete audit of all Lambda execution roles."""
    print("[*] Starting Lambda execution role audit...")
    functions = list_lambda_roles(region)
    all_findings = []
    audited_roles = set()

    for func in functions:
        role_name = func["role_name"]
        if role_name in audited_roles:
            continue
        audited_roles.add(role_name)
        print(f"\n[*] Auditing role: {role_name}")
        all_findings.extend(audit_role_permissions(role_name))
        all_findings.extend(check_trust_policy(role_name))
        has_boundary = check_permission_boundary(role_name)
        if not has_boundary:
            all_findings.append({
                "type": "NO_PERMISSION_BOUNDARY", "severity": "MEDIUM",
                "role": role_name, "detail": "No permission boundary applied",
            })

    critical = sum(1 for f in all_findings if f["severity"] == "CRITICAL")
    high = sum(1 for f in all_findings if f["severity"] == "HIGH")
    medium = sum(1 for f in all_findings if f["severity"] == "MEDIUM")
    print(f"\n[*] Audit complete: {len(all_findings)} findings "
          f"(Critical: {critical}, High: {high}, Medium: {medium})")
    return all_findings


def main():
    parser = argparse.ArgumentParser(description="AWS Lambda Execution Role Security Agent")
    parser.add_argument("action", choices=["list", "audit-role", "full-audit", "check-boundary",
                                           "check-trust", "validate-policy"])
    parser.add_argument("--role", help="Role name to audit")
    parser.add_argument("--region", default="us-east-1")
    parser.add_argument("--policy-file", help="Policy JSON file to validate")
    parser.add_argument("-o", "--output", default="lambda_role_audit.json")
    args = parser.parse_args()

    if args.action == "list":
        list_lambda_roles(args.region)
    elif args.action == "audit-role":
        audit_role_permissions(args.role)
    elif args.action == "full-audit":
        findings = full_audit(args.region)
        with open(args.output, "w") as f:
            json.dump(findings, f, indent=2, default=str)
        print(f"[*] Report saved to {args.output}")
    elif args.action == "check-boundary":
        check_permission_boundary(args.role)
    elif args.action == "check-trust":
        check_trust_policy(args.role)
    elif args.action == "validate-policy":
        with open(args.policy_file) as f:
            doc = json.load(f)
        validate_with_access_analyzer(doc, args.region)


if __name__ == "__main__":
    main()
Keep exploring