Module 5: AWS Services for AI (S3, Lambda, SageMaker Basics)

6. IAM and Least Privilege

Overview

In this capsule you'll learn to configure IAM (Identity and Access Management) so your AI system on AWS is secure in production. In LocalStack there's no real IAM — everything works without permissions. In AWS, every action requires explicit authorization. Your Lambda needs permission to read from S3. Your Lambda needs permission to invoke SageMaker. And those permissions must be the minimum necessary — no more. A Lambda with an admin role in production is a security incident waiting to happen.

Context: You've built the S3 + Lambda pipeline in the previous capsules. The code works, but on real AWS it needs permissions to operate. This capsule teaches you to create IAM roles with least privilege: your Lambda can read from ai-assets-dev/documents/* but not from ai-assets-dev/secrets/*. It can invoke gpt-4o-mini but not delete the bucket. By the end, your service will be functional AND secure.


IAM Concepts for AI Engineers

The three pillars of IAM

IAM (Identity and Access Management):
├── Users & Groups     → Who you are (people, teams)
├── Roles              → What identity a service (Lambda, EC2) can assume
└── Policies           → What permissions that identity has

For your AI Lambda:
├── Lambda assumes a Role
├── The Role has Policies attached
└── The Policies define what it can do
    ├── ✅ s3:GetObject on ai-assets-dev/documents/*
    ├── ✅ s3:PutObject on ai-assets-dev/responses/*
    ├── ❌ s3:DeleteBucket (doesn't need this)
    └── ❌ iam:* (definitely not)

Principal: who acts

# A Principal is the entity that performs an action
# For Lambda, the principal is the service lambda.amazonaws.com

# This allows Lambda to assume the role
trust_policy = {
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {"Service": "lambda.amazonaws.com"},
            "Action": "sts:AssumeRole",
        }
    ],
}

The anatomy of a Policy

policy = {
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "ReadDocumentsFromS3",        # Descriptive identifier
            "Effect": "Allow",                    # Allow or Deny
            "Action": [                           # Which operations
                "s3:GetObject",
                "s3:ListBucket",
            ],
            "Resource": [                         # On which resources
                "arn:aws:s3:::ai-assets-dev",
                "arn:aws:s3:::ai-assets-dev/documents/*",
            ],
        }
    ],
}

# Each Statement has:
# Effect:   Allow (permits) or Deny (denies, takes priority)
# Action:   AWS API operations (s3:GetObject, lambda:InvokeFunction)
# Resource: ARN of the specific resource (don't use * unless necessary)

Least Privilege: The Fundamental Principle

What least privilege means

Your Lambda should only be able to do what it needs to function. Nothing more.

❌ BAD: Policy too permissive
{
    "Effect": "Allow",
    "Action": "s3:*",           ← Can do ANYTHING in S3
    "Resource": "*"             ← In ANY bucket in the account
}

❌ WORSE: Admin access
{
    "Effect": "Allow",
    "Action": "*",              ← Can do ANYTHING
    "Resource": "*"             ← On ANY resource
}

✅ GOOD: Only what's necessary
{
    "Effect": "Allow",
    "Action": ["s3:GetObject"],     ← Only read objects
    "Resource": [
        "arn:aws:s3:::ai-assets-dev/documents/*",   ← Only this prefix
        "arn:aws:s3:::ai-assets-dev/prompts/*"       ← And this one
    ]
}

Why it matters for AI

Imagine your Lambda has s3:* on *. If an attacker compromises your LLM provider's API key and can execute arbitrary code (prompt injection → code execution in the worst case), they can:

  • Read ALL the buckets in your account (sensitive data, credentials)
  • Delete buckets (sabotage)
  • Upload malware to S3

With least privilege, the most they can do is read documents from a specific prefix and write responses. Contained damage.


Creating IAM Roles for Lambda

Role with boto3

import boto3
import json
import os

iam = boto3.client("iam", endpoint_url=os.environ.get("AWS_ENDPOINT_URL"))

ACCOUNT_ID = os.environ.get("AWS_ACCOUNT_ID", "123456789012")
BUCKET_NAME = os.environ.get("AI_BUCKET", "ai-assets-dev")


def create_lambda_role(role_name: str) -> str:
    """Creates an IAM role that Lambda can assume."""
    trust_policy = {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Effect": "Allow",
                "Principal": {"Service": "lambda.amazonaws.com"},
                "Action": "sts:AssumeRole",
            }
        ],
    }

    response = iam.create_role(
        RoleName=role_name,
        AssumeRolePolicyDocument=json.dumps(trust_policy),
        Description="Role for AI inference Lambda function",
    )

    role_arn = response["Role"]["Arn"]
    print(f"Role created: {role_arn}")
    return role_arn

Specific policies for an AI Lambda

def attach_s3_read_policy(role_name: str, bucket: str, prefixes: list[str]):
    """Attaches a policy to read objects from specific prefixes in S3."""
    resources = [f"arn:aws:s3:::{bucket}"]
    resources.extend(
        f"arn:aws:s3:::{bucket}/{prefix}*" for prefix in prefixes
    )

    policy = {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Sid": "ListBucket",
                "Effect": "Allow",
                "Action": ["s3:ListBucket"],
                "Resource": [f"arn:aws:s3:::{bucket}"],
                "Condition": {
                    "StringLike": {
                        "s3:prefix": [f"{p}*" for p in prefixes]
                    }
                },
            },
            {
                "Sid": "ReadObjects",
                "Effect": "Allow",
                "Action": ["s3:GetObject"],
                "Resource": [
                    f"arn:aws:s3:::{bucket}/{prefix}*" for prefix in prefixes
                ],
            },
        ],
    }

    iam.put_role_policy(
        RoleName=role_name,
        PolicyName="s3-read-ai-assets",
        PolicyDocument=json.dumps(policy),
    )
    print(f"S3 read policy attached to {role_name}")


def attach_s3_write_policy(role_name: str, bucket: str, prefixes: list[str]):
    """Attaches a policy to write objects to specific S3 prefixes."""
    policy = {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Sid": "WriteObjects",
                "Effect": "Allow",
                "Action": ["s3:PutObject"],
                "Resource": [
                    f"arn:aws:s3:::{bucket}/{prefix}*" for prefix in prefixes
                ],
            },
        ],
    }

    iam.put_role_policy(
        RoleName=role_name,
        PolicyName="s3-write-responses",
        PolicyDocument=json.dumps(policy),
    )
    print(f"S3 write policy attached to {role_name}")


def attach_cloudwatch_logs_policy(role_name: str):
    """Attaches a policy so Lambda can write logs to CloudWatch."""
    iam.attach_role_policy(
        RoleName=role_name,
        PolicyArn="arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole",
    )
    print(f"CloudWatch Logs policy attached to {role_name}")

Configure the complete role

def setup_ai_lambda_role() -> str:
    """Configures a complete IAM role for an AI inference Lambda."""
    role_name = "ai-inference-lambda-role"

    role_arn = create_lambda_role(role_name)

    # Lambda can READ prompts and documents
    attach_s3_read_policy(
        role_name,
        BUCKET_NAME,
        prefixes=["prompts/", "documents/", "config/"],
    )

    # Lambda can WRITE responses
    attach_s3_write_policy(
        role_name,
        BUCKET_NAME,
        prefixes=["responses/"],
    )

    # Lambda can write logs
    attach_cloudwatch_logs_policy(role_name)

    print(f"\nRole configured: {role_name}")
    print(f"  Read:  s3://{BUCKET_NAME}/prompts/*, documents/*, config/*")
    print(f"  Write: s3://{BUCKET_NAME}/responses/*")
    print(f"  Logs:  CloudWatch Logs")

    return role_arn

Inline vs Managed Policies

Types of policies

Managed Policies (AWS-managed):
├── AWSLambdaBasicExecutionRole    → CloudWatch logs
├── AmazonS3ReadOnlyAccess         → Read ALL of S3 (too broad)
├── AmazonS3FullAccess             → Everything in S3 (NEVER in production)
└── AmazonSageMakerFullAccess      → Everything in SageMaker (NEVER in production)

Managed Policies (customer-managed):
├── You create them
├── They're versioned (up to 5 versions)
├── They can be attached to multiple roles
└── Ideal for policies shared by several roles

Inline Policies:
├── You attach them directly to the role
├── Not shared between roles
├── Deleted when the role is deleted
└── Ideal for policies unique to a specific role

When to use each

TypeWhenExample
AWS ManagedWell-defined standard permissionsAWSLambdaBasicExecutionRole for logs
Customer ManagedPermissions shared across multiple rolesS3 read policy for 5 different Lambdas
InlinePermissions specific to a single roleLambda X can write ONLY to responses/

Customer Managed Policy

def create_ai_s3_managed_policy(bucket: str) -> str:
    """Creates a reusable managed policy for AI access to S3."""
    policy = {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Sid": "ReadAIAssets",
                "Effect": "Allow",
                "Action": ["s3:GetObject", "s3:ListBucket"],
                "Resource": [
                    f"arn:aws:s3:::{bucket}",
                    f"arn:aws:s3:::{bucket}/prompts/*",
                    f"arn:aws:s3:::{bucket}/documents/*",
                    f"arn:aws:s3:::{bucket}/embeddings/*",
                ],
            },
            {
                "Sid": "WriteResponses",
                "Effect": "Allow",
                "Action": ["s3:PutObject"],
                "Resource": [f"arn:aws:s3:::{bucket}/responses/*"],
            },
        ],
    }

    response = iam.create_policy(
        PolicyName="ai-lambda-s3-access",
        PolicyDocument=json.dumps(policy),
        Description="S3 access policy for AI Lambda functions",
    )

    policy_arn = response["Policy"]["Arn"]
    print(f"Managed policy created: {policy_arn}")
    return policy_arn

SAM Template with IAM Roles

Define permissions in template.yaml

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: AI Service with least-privilege IAM

Parameters:
  BucketName:
    Type: String
    Default: ai-assets-dev

Resources:
  AIBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Ref BucketName

  DocProcessorFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: ai-doc-processor
      Handler: handler.handler
      Runtime: python3.11
      MemorySize: 512
      Timeout: 120
      Policies:
        # SAM simplifies IAM with policy templates
        - S3ReadPolicy:
            BucketName: !Ref BucketName
        - S3CrudPolicy:
            BucketName: !Ref BucketName
        - Statement:
            - Sid: RestrictWriteToResponses
              Effect: Allow
              Action:
                - s3:PutObject
              Resource:
                - !Sub "arn:aws:s3:::${BucketName}/responses/*"
      Events:
        NewDocument:
          Type: S3
          Properties:
            Bucket: !Ref AIBucket
            Events: s3:ObjectCreated:*
            Filter:
              S3Key:
                Rules:
                  - Name: prefix
                    Value: documents/inbox/

SAM Policy Templates

SAM offers predefined policy templates that simplify IAM:

Policies:
  # Read from S3
  - S3ReadPolicy:
      BucketName: !Ref BucketName

  # Full CRUD on S3
  - S3CrudPolicy:
      BucketName: !Ref BucketName

  # Invoke another Lambda
  - LambdaInvokePolicy:
      FunctionName: !Ref OtherFunction

  # Invoke a SageMaker endpoint
  - Statement:
      - Effect: Allow
        Action: sagemaker:InvokeEndpoint
        Resource: !Sub "arn:aws:sagemaker:${AWS::Region}:${AWS::AccountId}:endpoint/*"

  # SSM Parameter Store
  - SSMParameterReadPolicy:
      ParameterName: /ai-service/*

  # Secrets Manager
  - Statement:
      - Effect: Allow
        Action: secretsmanager:GetSecretValue
        Resource: !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:ai-service/*"

Security Best Practices

IAM security checklist for AI

Lambda permissions:
├── ✅ Specific role per function (don't share roles between unrelated functions)
├── ✅ Only the necessary actions (s3:GetObject, not s3:*)
├── ✅ Specific resources (arn:aws:s3:::bucket/prefix/*, not *)
├── ✅ AWSLambdaBasicExecutionRole for logs
├── ❌ Don't use AmazonS3FullAccess
├── ❌ Don't use AdministratorAccess
└── ❌ Don't use Resource: "*" if you can be specific

Secrets:
├── ✅ API keys in Environment Variables (minimum) or SSM/Secrets Manager
├── ✅ Encryption at rest (enabled by default in Lambda env vars)
├── ❌ Don't hardcode API keys in code
├── ❌ Don't commit .env to Git
└── ❌ Don't share keys between environments (dev, staging, prod)

S3 buckets:
├── ✅ Block Public Access enabled (default on new buckets)
├── ✅ Encryption at rest (SSE-S3 or SSE-KMS)
├── ✅ Versioning for critical assets (prompt templates)
├── ❌ Don't enable public access unless explicitly necessary
└── ❌ Don't use ACLs (use policies instead)

Permission auditing

def audit_lambda_permissions(function_name: str):
    """Audits the permissions of a Lambda function."""
    lambda_client = boto3.client("lambda")

    config = lambda_client.get_function_configuration(FunctionName=function_name)
    role_arn = config["Role"]
    role_name = role_arn.split("/")[-1]

    print(f"Function: {function_name}")
    print(f"Role: {role_name}")
    print()

    # Managed policies
    attached = iam.list_attached_role_policies(RoleName=role_name)
    print("Managed policies:")
    for policy in attached["AttachedPolicies"]:
        print(f"  - {policy['PolicyName']}")
        if "FullAccess" in policy["PolicyName"]:
            print(f"    ⚠️ WARNING: FullAccess policy detected!")
    print()

    # Inline policies
    inline = iam.list_role_policies(RoleName=role_name)
    print("Inline policies:")
    for policy_name in inline["PolicyNames"]:
        policy_doc = iam.get_role_policy(RoleName=role_name, PolicyName=policy_name)
        document = policy_doc["PolicyDocument"]

        for stmt in document.get("Statement", []):
            actions = stmt.get("Action", [])
            resources = stmt.get("Resource", [])
            if isinstance(actions, str):
                actions = [actions]
            if isinstance(resources, str):
                resources = [resources]

            has_wildcard_action = any("*" in a for a in actions)
            has_wildcard_resource = any(r == "*" for r in resources)

            print(f"  {policy_name}:")
            print(f"    Actions: {actions}")
            print(f"    Resources: {resources}")
            if has_wildcard_action:
                print(f"    ⚠️ WARNING: Wildcard action detected!")
            if has_wildcard_resource:
                print(f"    ⚠️ WARNING: Wildcard resource detected!")
    print()

Deny Policies: Additional Protection

def attach_deny_dangerous_actions(role_name: str):
    """Attaches a deny policy to protect against dangerous actions."""
    deny_policy = {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Sid": "DenyDangerousActions",
                "Effect": "Deny",
                "Action": [
                    "s3:DeleteBucket",
                    "s3:PutBucketPolicy",
                    "iam:*",
                    "organizations:*",
                    "account:*",
                ],
                "Resource": "*",
            },
            {
                "Sid": "DenyDeleteProductionAssets",
                "Effect": "Deny",
                "Action": ["s3:DeleteObject"],
                "Resource": [
                    "arn:aws:s3:::ai-assets-prod/prompts/*",
                    "arn:aws:s3:::ai-assets-prod/models/*",
                ],
            },
        ],
    }

    iam.put_role_policy(
        RoleName=role_name,
        PolicyName="deny-dangerous-actions",
        PolicyDocument=json.dumps(deny_policy),
    )
    print(f"Deny policy attached to {role_name}")

Troubleshooting

Problem 1: "AccessDenied" when calling s3:GetObject

Your Lambda doesn't have permission to read from the bucket or prefix.

# Check the role's policies
aws iam list-role-policies --role-name ai-inference-lambda-role
aws iam get-role-policy --role-name ai-inference-lambda-role --policy-name s3-read-ai-assets

# Verify that the Resource includes the correct prefix
# "arn:aws:s3:::ai-assets-dev/documents/*" ← includes the /*

Problem 2: "is not authorized to perform: sts:AssumeRole"

The role's trust policy doesn't include Lambda as a principal.

# Check the trust policy
response = iam.get_role(RoleName="ai-inference-lambda-role")
print(json.dumps(response["Role"]["AssumeRolePolicyDocument"], indent=2))

# It must include:
# "Principal": {"Service": "lambda.amazonaws.com"}

Problem 3: Lambda works in LocalStack but fails on AWS with permissions

LocalStack doesn't enforce IAM by default. Your code works without permissions locally but fails on real AWS.

# In LocalStack: IAM is not enforced (everything is allowed)
# On AWS: every operation is validated against the role's policy

# Solution: Before migrating from LocalStack to AWS,
# define the necessary policies and test them with the IAM Policy Simulator:
# https://policysim.aws.amazon.com/

Problem 4: Policy too large

Inline policies have a 10KB limit. If you have many prefixes, use managed policies.

# Inline policy limit: 10,240 characters
# Managed policy limit: 6,144 characters per version, but can have 5 versions
# If you need more: use conditions instead of listing all the ARNs

Practical Exercises

Exercise 1: Create a least-privilege policy for a RAG service

Your RAG service needs to: read documents from documents/, read embeddings from embeddings/, read prompts from prompts/, write responses to responses/, and NOT be able to access models/ or delete anything. Create the complete policy.

See solution
import boto3
import json
import os

iam = boto3.client("iam", endpoint_url=os.environ.get("AWS_ENDPOINT_URL"))
BUCKET = "ai-assets-prod"


def create_rag_service_policy(role_name: str):
    """Creates a least-privilege policy for a RAG service."""
    policy = {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Sid": "ListBucketWithPrefixes",
                "Effect": "Allow",
                "Action": ["s3:ListBucket"],
                "Resource": [f"arn:aws:s3:::{BUCKET}"],
                "Condition": {
                    "StringLike": {
                        "s3:prefix": [
                            "documents/*",
                            "embeddings/*",
                            "prompts/*",
                            "responses/*",
                        ]
                    }
                },
            },
            {
                "Sid": "ReadAIAssets",
                "Effect": "Allow",
                "Action": ["s3:GetObject", "s3:HeadObject"],
                "Resource": [
                    f"arn:aws:s3:::{BUCKET}/documents/*",
                    f"arn:aws:s3:::{BUCKET}/embeddings/*",
                    f"arn:aws:s3:::{BUCKET}/prompts/*",
                ],
            },
            {
                "Sid": "WriteResponses",
                "Effect": "Allow",
                "Action": ["s3:PutObject"],
                "Resource": [f"arn:aws:s3:::{BUCKET}/responses/*"],
            },
            {
                "Sid": "DenyModelsAccess",
                "Effect": "Deny",
                "Action": ["s3:*"],
                "Resource": [f"arn:aws:s3:::{BUCKET}/models/*"],
            },
            {
                "Sid": "DenyDeleteOperations",
                "Effect": "Deny",
                "Action": [
                    "s3:DeleteObject",
                    "s3:DeleteBucket",
                    "s3:PutBucketPolicy",
                ],
                "Resource": [
                    f"arn:aws:s3:::{BUCKET}",
                    f"arn:aws:s3:::{BUCKET}/*",
                ],
            },
        ],
    }

    iam.put_role_policy(
        RoleName=role_name,
        PolicyName="rag-service-s3-policy",
        PolicyDocument=json.dumps(policy),
    )
    print(f"RAG service policy attached to {role_name}")
    print(f"  ✅ Read:  documents/*, embeddings/*, prompts/*")
    print(f"  ✅ Write: responses/*")
    print(f"  ❌ Deny:  models/*, delete operations")

create_rag_service_policy("ai-inference-lambda-role")

Exercise 2: Audit an existing role

Write a function that takes a role name and returns a detailed report of all permissions, flagging the ones that are too broad (wildcards in actions or resources).

See solution
import boto3
import json
import os

iam = boto3.client("iam", endpoint_url=os.environ.get("AWS_ENDPOINT_URL"))


def audit_role(role_name: str) -> dict:
    """Audits an IAM role and returns a security report."""
    report = {
        "role_name": role_name,
        "managed_policies": [],
        "inline_policies": [],
        "warnings": [],
        "score": "secure",
    }

    attached = iam.list_attached_role_policies(RoleName=role_name)
    for p in attached["AttachedPolicies"]:
        name = p["PolicyName"]
        report["managed_policies"].append(name)
        if "FullAccess" in name:
            report["warnings"].append(f"FullAccess managed policy: {name}")
        if name == "AdministratorAccess":
            report["warnings"].append(f"CRITICAL: AdministratorAccess attached!")
            report["score"] = "critical"

    inline_names = iam.list_role_policies(RoleName=role_name)["PolicyNames"]
    for policy_name in inline_names:
        doc = iam.get_role_policy(RoleName=role_name, PolicyName=policy_name)
        policy_doc = doc["PolicyDocument"]

        for stmt in policy_doc.get("Statement", []):
            actions = stmt.get("Action", [])
            resources = stmt.get("Resource", [])
            effect = stmt.get("Effect", "Allow")

            if isinstance(actions, str):
                actions = [actions]
            if isinstance(resources, str):
                resources = [resources]

            for action in actions:
                if action == "*" and effect == "Allow":
                    report["warnings"].append(f"Wildcard action in {policy_name}")
                    report["score"] = "critical"
                elif ":*" in action and effect == "Allow":
                    report["warnings"].append(f"Service wildcard: {action} in {policy_name}")
                    if report["score"] != "critical":
                        report["score"] = "warning"

            for resource in resources:
                if resource == "*" and effect == "Allow":
                    report["warnings"].append(f"Wildcard resource in {policy_name}")
                    if report["score"] != "critical":
                        report["score"] = "warning"

            report["inline_policies"].append({
                "name": policy_name,
                "effect": effect,
                "actions": actions,
                "resources": resources,
            })

    return report


report = audit_role("ai-inference-lambda-role")
print(json.dumps(report, indent=2, default=str))

if report["warnings"]:
    print(f"\n⚠️  {len(report['warnings'])} warning(s) found:")
    for w in report["warnings"]:
        print(f"  - {w}")
else:
    print("\n✅ No security warnings found")

Exercise 3: Policy for a Lambda that invokes SageMaker

Create a policy that lets your Lambda invoke ONE specific SageMaker endpoint and read from one specific S3 bucket, but nothing else.

See solution
import boto3
import json
import os

iam = boto3.client("iam", endpoint_url=os.environ.get("AWS_ENDPOINT_URL"))

ACCOUNT_ID = os.environ.get("AWS_ACCOUNT_ID", "123456789012")
REGION = os.environ.get("AWS_DEFAULT_REGION", "us-east-1")


def create_lambda_sagemaker_policy(
    role_name: str,
    endpoint_name: str,
    bucket: str,
    read_prefixes: list[str],
):
    """Policy for a Lambda that invokes SageMaker + reads S3."""
    endpoint_arn = f"arn:aws:sagemaker:{REGION}:{ACCOUNT_ID}:endpoint/{endpoint_name}"

    policy = {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Sid": "InvokeSageMakerEndpoint",
                "Effect": "Allow",
                "Action": ["sagemaker:InvokeEndpoint"],
                "Resource": [endpoint_arn],
            },
            {
                "Sid": "ReadFromS3",
                "Effect": "Allow",
                "Action": ["s3:GetObject"],
                "Resource": [
                    f"arn:aws:s3:::{bucket}/{prefix}*"
                    for prefix in read_prefixes
                ],
            },
            {
                "Sid": "DenyOtherSageMaker",
                "Effect": "Deny",
                "Action": [
                    "sagemaker:CreateEndpoint",
                    "sagemaker:DeleteEndpoint",
                    "sagemaker:UpdateEndpoint",
                ],
                "Resource": "*",
            },
        ],
    }

    iam.put_role_policy(
        RoleName=role_name,
        PolicyName="lambda-sagemaker-inference",
        PolicyDocument=json.dumps(policy),
    )
    print(f"SageMaker inference policy attached to {role_name}")
    print(f"  ✅ Can invoke: {endpoint_name}")
    print(f"  ❌ Cannot create/delete/update endpoints")


create_lambda_sagemaker_policy(
    role_name="ai-inference-lambda-role",
    endpoint_name="sentiment-classifier-v1",
    bucket="ai-assets-prod",
    read_prefixes=["documents/", "embeddings/"],
)

Exercise 4: Complete setup script for a new AI service

Create a script that configures all the IAM needed for a new AI service: role, trust policy, S3 policies (read + write), logs policy, and a security deny policy. The script must be idempotent (running it twice doesn't fail).

See solution
import boto3
import json
import os

iam = boto3.client("iam", endpoint_url=os.environ.get("AWS_ENDPOINT_URL"))


def setup_ai_service_iam(
    service_name: str,
    bucket: str,
    read_prefixes: list[str],
    write_prefixes: list[str],
) -> str:
    """Complete IAM setup for an AI service. Idempotent."""
    role_name = f"{service_name}-lambda-role"

    # 1. Create role (idempotent)
    trust_policy = {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Effect": "Allow",
                "Principal": {"Service": "lambda.amazonaws.com"},
                "Action": "sts:AssumeRole",
            }
        ],
    }

    try:
        response = iam.create_role(
            RoleName=role_name,
            AssumeRolePolicyDocument=json.dumps(trust_policy),
            Description=f"Lambda role for {service_name}",
        )
        role_arn = response["Role"]["Arn"]
        print(f"✅ Role created: {role_name}")
    except iam.exceptions.EntityAlreadyExistsException:
        response = iam.get_role(RoleName=role_name)
        role_arn = response["Role"]["Arn"]
        print(f"ℹ️  Role already exists: {role_name}")

    # 2. S3 read policy
    read_policy = {
        "Version": "2012-10-17",
        "Statement": [{
            "Sid": "ReadAIAssets",
            "Effect": "Allow",
            "Action": ["s3:GetObject", "s3:HeadObject", "s3:ListBucket"],
            "Resource": [f"arn:aws:s3:::{bucket}"]
            + [f"arn:aws:s3:::{bucket}/{p}*" for p in read_prefixes],
        }],
    }
    iam.put_role_policy(
        RoleName=role_name,
        PolicyName=f"{service_name}-s3-read",
        PolicyDocument=json.dumps(read_policy),
    )
    print(f"✅ S3 read policy: {read_prefixes}")

    # 3. S3 write policy
    write_policy = {
        "Version": "2012-10-17",
        "Statement": [{
            "Sid": "WriteResults",
            "Effect": "Allow",
            "Action": ["s3:PutObject"],
            "Resource": [f"arn:aws:s3:::{bucket}/{p}*" for p in write_prefixes],
        }],
    }
    iam.put_role_policy(
        RoleName=role_name,
        PolicyName=f"{service_name}-s3-write",
        PolicyDocument=json.dumps(write_policy),
    )
    print(f"✅ S3 write policy: {write_prefixes}")

    # 4. CloudWatch Logs
    try:
        iam.attach_role_policy(
            RoleName=role_name,
            PolicyArn="arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole",
        )
        print("✅ CloudWatch Logs policy attached")
    except iam.exceptions.ClientError:
        print("ℹ️  CloudWatch Logs policy already attached")

    # 5. Deny dangerous actions
    deny_policy = {
        "Version": "2012-10-17",
        "Statement": [{
            "Sid": "DenyDangerous",
            "Effect": "Deny",
            "Action": ["s3:DeleteBucket", "iam:*", "s3:PutBucketPolicy"],
            "Resource": "*",
        }],
    }
    iam.put_role_policy(
        RoleName=role_name,
        PolicyName=f"{service_name}-deny-dangerous",
        PolicyDocument=json.dumps(deny_policy),
    )
    print("✅ Deny policy for dangerous actions")

    print(f"\n{'='*50}")
    print(f"Service: {service_name}")
    print(f"Role ARN: {role_arn}")
    print(f"Read:  {', '.join(read_prefixes)}")
    print(f"Write: {', '.join(write_prefixes)}")
    print(f"{'='*50}")

    return role_arn


setup_ai_service_iam(
    service_name="rag-service",
    bucket="ai-assets-prod",
    read_prefixes=["documents/", "embeddings/", "prompts/"],
    write_prefixes=["responses/"],
)

Summary

  • IAM controls who can do what on AWS. For Lambda, you define a Role with Policies that specify the allowed actions and resources.
  • Least privilege: only the minimum necessary permissions. s3:GetObject on bucket/prefix/*, not s3:* on *.
  • The trust policy lets Lambda assume the role. Permission policies define what it can do with that role.
  • Inline policies for permissions specific to a role. Managed policies for permissions shared across roles.
  • Deny policies protect against dangerous actions (delete bucket, IAM changes) even if another policy allows them.
  • LocalStack doesn't enforce IAM. Your policies are validated when you migrate to real AWS. Define policies before migrating.
  • SAM Policy Templates simplify IAM in templates — S3ReadPolicy, S3CrudPolicy, LambdaInvokePolicy.

Additional Resources

  1. IAM Best Practices — Official best practices
  2. IAM Policy Reference — Complete policy reference
  3. IAM Policy Simulator — Tool to validate policies
  4. SAM Policy Templates — Predefined templates
  5. S3 Bucket Policies — Bucket-level policies
  6. Lambda Execution Role — Lambda execution roles
  7. IAM Access Analyzer — Tool to detect excessive permissions
  8. AWS Security Hub — Centralized security monitoring