Setting up IAM roles and policies
Scope: Security best practices - IAM, security groups, secrets management, encryption Lines: 381 Last Updated: 2025-10-18 Format Version: 1.0 (Atomic)
Activate when:
Prerequisites:
Common scenarios:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "LambdaDynamoDBAccess",
"Effect": "Allow",
"Action": [
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:UpdateItem",
"dynamodb:Query"
],
"Resource": [
"arn:aws:dynamodb:us-east-1:123456789012:table/Users",
"arn:aws:dynamodb:us-east-1:123456789012:table/Users/index/*"
]
},
{
"Sid": "S3ReadAccess",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::my-bucket",
"arn:aws:s3:::my-bucket/*"
],
"Condition": {
"StringLike": {
"s3:prefix": ["public/*"]
}
}
},
{
"Sid": "SecretsManagerAccess",
"Effect": "Allow",
"Action": [
"secretsmanager:GetSecretValue"
],
"Resource": "arn:aws:secretsmanager:us-east-1:123456789012:secret:db-credentials-*"
},
{
"Sid": "CloudWatchLogs",
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:*:*:*"
}
]
}
// Trust policy - who can assume this role
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
// Cross-account access trust policy
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::987654321098:root"
},
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {
"sts:ExternalId": "unique-external-id-12345"
}
}
}
]
}
# Terraform - Web tier security group
resource "aws_security_group" "web" {
name = "web-tier-sg"
description = "Security group for web tier"
vpc_id = var.vpc_id
# Inbound rules
ingress {
description = "HTTPS from internet"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "HTTP from internet"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
# Outbound rules
egress {
description = "To app tier"
from_port = 8080
to_port = 8080
protocol = "tcp"
security_groups = [aws_security_group.app.id]
}
egress {
description = "HTTPS to internet"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "web-tier-sg"
Tier = "web"
}
}
# Application tier security group
resource "aws_security_group" "app" {
name = "app-tier-sg"
description = "Security group for app tier"
vpc_id = var.vpc_id
ingress {
description = "From web tier"
from_port = 8080
to_port = 8080
protocol = "tcp"
security_groups = [aws_security_group.web.id]
}
egress {
description = "To database"
from_port = 5432
to_port = 5432
protocol = "tcp"
security_groups = [aws_security_group.database.id]
}
tags = {
Name = "app-tier-sg"
Tier = "app"
}
}
# Database tier security group
resource "aws_security_group" "database" {
name = "database-tier-sg"
description = "Security group for database tier"
vpc_id = var.vpc_id
ingress {
description = "PostgreSQL from app tier"
from_port = 5432
to_port = 5432
protocol = "tcp"
security_groups = [aws_security_group.app.id]
}
# No outbound rules - database doesn't initiate connections
tags = {
Name = "database-tier-sg"
Tier = "database"
}
}
# AWS Secrets Manager
import boto3
import json
from botocore.exceptions import ClientError
def get_secret(secret_name, region_name="us-east-1"):
"""Retrieve secret from AWS Secrets Manager"""
session = boto3.session.Session()
client = session.client(
service_name='secretsmanager',
region_name=region_name
)
try:
response = client.get_secret_value(SecretId=secret_name)
if 'SecretString' in response:
return json.loads(response['SecretString'])
else:
# Binary secret
return response['SecretBinary']
except ClientError as e:
if e.response['Error']['Code'] == 'ResourceNotFoundException':
print(f"Secret {secret_name} not found")
elif e.response['Error']['Code'] == 'InvalidRequestException':
print(f"Invalid request for secret {secret_name}")
elif e.response['Error']['Code'] == 'InvalidParameterException':
print(f"Invalid parameter for secret {secret_name}")
raise e
# Lambda function using secrets
import os
def lambda_handler(event, context):
# Get database credentials
db_secret = get_secret(os.environ['DB_SECRET_NAME'])
# Use credentials
db_host = db_secret['host']
db_user = db_secret['username']
db_password = db_secret['password']
# Connect to database
# ...
# Create/update secret
def create_secret(secret_name, secret_value, region_name="us-east-1"):
"""Create or update a secret"""
client = boto3.client('secretsmanager', region_name=region_name)
try:
response = client.create_secret(
Name=secret_name,
SecretString=json.dumps(secret_value),
Tags=[
{'Key': 'Environment', 'Value': 'production'},
{'Key': 'ManagedBy', 'Value': 'terraform'}
]
)
return response['ARN']
except client.exceptions.ResourceExistsException:
# Update existing secret
response = client.update_secret(
SecretId=secret_name,
SecretString=json.dumps(secret_value)
)
return response['ARN']
# Rotate secret
def rotate_secret(secret_name):
"""Rotate secret (requires Lambda rotation function)"""
client = boto3.client('secretsmanager')
response = client.rotate_secret(
SecretId=secret_name,
RotationLambdaARN='arn:aws:lambda:us-east-1:123456789012:function:rotate-secret',
RotationRules={
'AutomaticallyAfterDays': 30
}
)
return response
# S3 encryption at rest
import boto3
s3 = boto3.client('s3')
# Server-side encryption with S3-managed keys (SSE-S3)
s3.put_object(
Bucket='my-bucket',
Key='file.txt',
Body=b'data',
ServerSideEncryption='AES256'
)
# Server-side encryption with KMS (SSE-KMS)
s3.put_object(
Bucket='my-bucket',
Key='file.txt',
Body=b'data',
ServerSideEncryption='aws:kms',
SSEKMSKeyId='arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012'
)
# Client-side encryption
from cryptography.fernet import Fernet
# Generate key (store in Secrets Manager)
key = Fernet.generate_key()
cipher = Fernet(key)
# Encrypt data
plaintext = b"sensitive data"
ciphertext = cipher.encrypt(plaintext)
# Upload encrypted data
s3.put_object(
Bucket='my-bucket',
Key='encrypted-file.txt',
Body=ciphertext
)
# Decrypt data
encrypted_data = s3.get_object(Bucket='my-bucket', Key='encrypted-file.txt')['Body'].read()
decrypted_data = cipher.decrypt(encrypted_data)
# Terraform - Network ACL for public subnet
resource "aws_network_acl" "public" {
vpc_id = var.vpc_id
subnet_ids = var.public_subnet_ids
# Inbound rules
ingress {
rule_no = 100
protocol = "tcp"
action = "allow"
cidr_block = "0.0.0.0/0"
from_port = 443
to_port = 443
}
ingress {
rule_no = 110
protocol = "tcp"
action = "allow"
cidr_block = "0.0.0.0/0"
from_port = 80
to_port = 80
}
ingress {
rule_no = 120
protocol = "tcp"
action = "allow"
cidr_block = "0.0.0.0/0"
from_port = 1024
to_port = 65535 # Ephemeral ports for return traffic
}
# Deny all other inbound
ingress {
rule_no = 32767
protocol = "-1"
action = "deny"
cidr_block = "0.0.0.0/0"
from_port = 0
to_port = 0
}
# Outbound rules
egress {
rule_no = 100
protocol = "-1"
action = "allow"
cidr_block = "0.0.0.0/0"
from_port = 0
to_port = 0
}
tags = {
Name = "public-nacl"
}
}
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "SpecificResourceAccess",
"Effect": "Allow",
"Action": [
"dynamodb:GetItem",
"dynamodb:PutItem"
],
"Resource": "arn:aws:dynamodb:us-east-1:123456789012:table/Users",
"Condition": {
"ForAllValues:StringEquals": {
"dynamodb:LeadingKeys": ["${aws:username}"]
}
}
},
{
"Sid": "DenyUnencryptedUploads",
"Effect": "Deny",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::my-bucket/*",
"Condition": {
"StringNotEquals": {
"s3:x-amz-server-side-encryption": "aws:kms"
}
}
}
]
}
// Require MFA for sensitive operations
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowViewAccountInfo",
"Effect": "Allow",
"Action": [
"iam:GetAccountPasswordPolicy",
"iam:ListVirtualMFADevices"
],
"Resource": "*"
},
{
"Sid": "RequireMFAForSensitiveOperations",
"Effect": "Allow",
"Action": [
"ec2:TerminateInstances",
"rds:DeleteDBInstance",
"s3:DeleteBucket"
],
"Resource": "*",
"Condition": {
"Bool": {
"aws:MultiFactorAuthPresent": "true"
},
"NumericLessThan": {
"aws:MultiFactorAuthAge": "3600"
}
}
}
]
}
# Private access to AWS services without internet gateway
resource "aws_vpc_endpoint" "s3" {
vpc_id = var.vpc_id
service_name = "com.amazonaws.us-east-1.s3"
route_table_ids = var.private_route_table_ids
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "AllowS3Access"
Effect = "Allow"
Principal = "*"
Action = [
"s3:GetObject",
"s3:PutObject"
]
Resource = "arn:aws:s3:::my-private-bucket/*"
}
]
})
tags = {
Name = "s3-vpc-endpoint"
}
}
# Interface endpoint for other services
resource "aws_vpc_endpoint" "secretsmanager" {
vpc_id = var.vpc_id
service_name = "com.amazonaws.us-east-1.secretsmanager"
vpc_endpoint_type = "Interface"
subnet_ids = var.private_subnet_ids
security_group_ids = [aws_security_group.vpc_endpoint.id]
private_dns_enabled = true
tags = {
Name = "secretsmanager-vpc-endpoint"
}
}
# AWS Certificate Manager
import boto3
acm = boto3.client('acm')
# Request certificate
response = acm.request_certificate(
DomainName='example.com',
SubjectAlternativeNames=[
'*.example.com',
'www.example.com'
],
ValidationMethod='DNS',
Tags=[
{'Key': 'Environment', 'Value': 'production'}
]
)
certificate_arn = response['CertificateArn']
# Get certificate validation records
response = acm.describe_certificate(CertificateArn=certificate_arn)
validation_options = response['Certificate']['DomainValidationOptions']
# Create Route53 validation records automatically
route53 = boto3.client('route53')
for option in validation_options:
route53.change_resource_record_sets(
HostedZoneId='Z1234567890ABC',
ChangeBatch={
'Changes': [{
'Action': 'CREATE',
'ResourceRecordSet': {
'Name': option['ResourceRecord']['Name'],
'Type': option['ResourceRecord']['Type'],
'TTL': 300,
'ResourceRecords': [{'Value': option['ResourceRecord']['Value']}]
}
}]
}
)
# Lambda authorizer for API Gateway
import json
def lambda_handler(event, context):
"""
Custom authorizer for API Gateway
Validates JWT or API key
"""
token = event['authorizationToken'] # Bearer token
method_arn = event['methodArn']
# Validate token (check signature, expiration, etc.)
if validate_token(token):
principal_id = extract_user_id(token)
policy = generate_policy(principal_id, 'Allow', method_arn)
# Add context to pass to Lambda function
policy['context'] = {
'userId': principal_id,
'role': extract_role(token)
}
return policy
else:
raise Exception('Unauthorized')
def generate_policy(principal_id, effect, resource):
"""Generate IAM policy"""
policy = {
'principalId': principal_id,
'policyDocument': {
'Version': '2012-10-17',
'Statement': [{
'Action': 'execute-api:Invoke',
'Effect': effect,
'Resource': resource
}]
}
}
return policy
def validate_token(token):
"""Validate JWT token"""
# Implementation depends on your token type
# For JWT: decode, verify signature, check expiration
return True # Simplified
def extract_user_id(token):
"""Extract user ID from token"""
return "user123" # Simplified
def extract_role(token):
"""Extract role from token"""
return "admin" # Simplified
# Enable MFA for root account
# Create IAM users instead of using root
# Use groups to assign permissions
# Grant least privilege
# Use roles for applications
# Rotate credentials regularly
# Enable CloudTrail for auditing
# AWS CLI
aws iam create-user --user-name developer
aws iam create-group --group-name developers
aws iam add-user-to-group --user-name developer --group-name developers
aws iam attach-group-policy --group-name developers --policy-arn arn:aws:iam::aws:policy/ReadOnlyAccess
# Create access key
aws iam create-access-key --user-name developer
# Rotate access key
aws iam update-access-key --access-key-id AKIAIOSFODNN7EXAMPLE --status Inactive --user-name developer
aws iam delete-access-key --access-key-id AKIAIOSFODNN7EXAMPLE --user-name developer
[ ] IAM users have MFA enabled
[ ] Root account not used for daily tasks
[ ] Least privilege policies applied
[ ] Security groups follow principle of least access
[ ] Encryption at rest enabled (S3, EBS, RDS)
[ ] Encryption in transit enforced (HTTPS, TLS)
[ ] Secrets stored in Secrets Manager/Parameter Store
[ ] CloudTrail enabled for audit logging
[ ] VPC Flow Logs enabled
[ ] Regular security scans (AWS Inspector, GuardDuty)
[ ] Backup and disaster recovery plan
[ ] Network segmentation (public/private subnets)
[ ] WAF configured for web applications
[ ] Regular security patches applied
// ❌ NEVER: Use wildcard permissions in production
{
"Effect": "Allow",
"Action": "*",
"Resource": "*"
}
// ✅ CORRECT: Specific permissions
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Resource": "arn:aws:s3:::my-bucket/path/*"
}
# ❌ NEVER: Hardcode credentials
AWS_ACCESS_KEY = "AKIAIOSFODNN7EXAMPLE"
AWS_SECRET_KEY = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
# ✅ CORRECT: Use IAM roles or environment variables
import boto3
# Uses IAM role credentials automatically
s3 = boto3.client('s3')
# Or environment variables
import os
access_key = os.environ['AWS_ACCESS_KEY_ID']
# ❌ NEVER: Open security groups to 0.0.0.0/0 for SSH
resource "aws_security_group_rule" "ssh" {
type = "ingress"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"] # Dangerous!
security_group_id = aws_security_group.example.id
}
# ✅ CORRECT: Restrict to specific IPs or use bastion
resource "aws_security_group_rule" "ssh" {
type = "ingress"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["10.0.0.0/16"] # Internal only
security_group_id = aws_security_group.example.id
}
Infrastructure:
aws-serverless.md - IAM roles for Lambda, API Gateway authorizersterraform-patterns.md - Infrastructure as Code for security resourceskubernetes-basics.md - RBAC, network policies, secretsNetworking:
mtls-implementation.md - Mutual TLS for service-to-service authtailscale-vpn.md - Zero-trust networkingStandards from CLAUDE.md:
Last Updated: 2025-10-18 Format Version: 1.0 (Atomic)