IAM and Security
AWS Identity and Access Management is both the most critical and most complex aspect of AWS security. Getting IAM wrong can expose your entire infrastructure to attack or lock you out of your own resources. Terraform helps by making IAM policies version-controlled and repeatable, but you still need to understand the principles of least privilege, role-based access, and AWS’s various authentication mechanisms.
We’ll explore IAM patterns that work well in production, from basic role creation to complex cross-account access and automated security controls.
IAM Role Patterns
Roles are the foundation of AWS security, providing temporary credentials without long-lived access keys:
# EC2 instance role for application servers
resource "aws_iam_role" "app_server" {
name = "${var.name_prefix}-app-server-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "ec2.amazonaws.com"
}
}
]
})
tags = {
Name = "${var.name_prefix}-app-server-role"
Environment = var.environment
}
}
# Instance profile for EC2
resource "aws_iam_instance_profile" "app_server" {
name = "${var.name_prefix}-app-server-profile"
role = aws_iam_role.app_server.name
}
# Policy for application access
resource "aws_iam_role_policy" "app_server" {
name = "${var.name_prefix}-app-server-policy"
role = aws_iam_role.app_server.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"s3:GetObject",
"s3:PutObject"
]
Resource = [
"${aws_s3_bucket.app_data.arn}/*"
]
},
{
Effect = "Allow"
Action = [
"secretsmanager:GetSecretValue"
]
Resource = [
aws_secretsmanager_secret.app_secrets.arn
]
}
]
})
}
Cross-Account Access Patterns
Multi-account architectures require careful cross-account role configuration:
# Cross-account role for CI/CD access
resource "aws_iam_role" "cicd_cross_account" {
name = "${var.name_prefix}-cicd-cross-account"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
AWS = "arn:aws:iam::${var.cicd_account_id}:root"
}
Condition = {
StringEquals = {
"sts:ExternalId" = var.external_id
}
StringLike = {
"aws:userid" = "AIDACKCEVSQ6C2EXAMPLE:*"
}
}
}
]
})
max_session_duration = 3600 # 1 hour
tags = {
Purpose = "CI/CD cross-account access"
}
}
# Policy for deployment permissions
resource "aws_iam_role_policy" "cicd_deployment" {
name = "${var.name_prefix}-cicd-deployment"
role = aws_iam_role.cicd_cross_account.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"ec2:DescribeInstances",
"ec2:DescribeImages",
"ec2:RunInstances",
"ec2:TerminateInstances",
"ec2:CreateTags"
]
Resource = "*"
Condition = {
StringEquals = {
"aws:RequestedRegion" = [var.aws_region]
}
}
},
{
Effect = "Allow"
Action = [
"ecs:UpdateService",
"ecs:DescribeServices",
"ecs:RegisterTaskDefinition"
]
Resource = "*"
}
]
})
}
Service-Linked Roles and Managed Policies
Use AWS managed policies where appropriate, but understand their implications:
# Attach AWS managed policy
resource "aws_iam_role_policy_attachment" "app_server_ssm" {
role = aws_iam_role.app_server.name
policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}
# Create service-linked role for ECS
resource "aws_iam_service_linked_role" "ecs" {
aws_service_name = "ecs.amazonaws.com"
description = "Service-linked role for ECS"
}
# Custom policy with specific permissions
resource "aws_iam_policy" "app_specific" {
name = "${var.name_prefix}-app-specific"
description = "Application-specific permissions"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:UpdateItem",
"dynamodb:DeleteItem"
]
Resource = [
aws_dynamodb_table.app_data.arn,
"${aws_dynamodb_table.app_data.arn}/index/*"
]
}
]
})
}
resource "aws_iam_role_policy_attachment" "app_specific" {
role = aws_iam_role.app_server.name
policy_arn = aws_iam_policy.app_specific.arn
}
User and Group Management
Manage users and groups for human access:
# Developer group with limited permissions
resource "aws_iam_group" "developers" {
name = "${var.name_prefix}-developers"
}
resource "aws_iam_group_policy" "developers" {
name = "${var.name_prefix}-developers-policy"
group = aws_iam_group.developers.name
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"ec2:Describe*",
"s3:ListBucket",
"s3:GetObject",
"logs:DescribeLogGroups",
"logs:DescribeLogStreams",
"logs:GetLogEvents"
]
Resource = "*"
},
{
Effect = "Allow"
Action = [
"sts:AssumeRole"
]
Resource = [
aws_iam_role.developer_assume_role.arn
]
}
]
})
}
# Users (typically managed outside Terraform in production)
resource "aws_iam_user" "developers" {
for_each = var.developer_users
name = each.key
path = "/developers/"
tags = {
Team = each.value.team
Role = "developer"
}
}
resource "aws_iam_user_group_membership" "developers" {
for_each = aws_iam_user.developers
user = each.value.name
groups = [aws_iam_group.developers.name]
}
Secrets Management Integration
Integrate with AWS Secrets Manager and Parameter Store:
# Application secrets in Secrets Manager
resource "aws_secretsmanager_secret" "app_secrets" {
name = "${var.name_prefix}/app/secrets"
description = "Application secrets"
replica {
region = var.backup_region
}
tags = {
Application = var.application_name
Environment = var.environment
}
}
resource "aws_secretsmanager_secret_version" "app_secrets" {
secret_id = aws_secretsmanager_secret.app_secrets.id
secret_string = jsonencode({
database_password = random_password.db_password.result
api_key = random_password.api_key.result
jwt_secret = random_password.jwt_secret.result
})
}
# Configuration in Parameter Store
resource "aws_ssm_parameter" "app_config" {
for_each = var.app_parameters
name = "/${var.name_prefix}/config/${each.key}"
type = each.value.secure ? "SecureString" : "String"
value = each.value.value
tags = {
Application = var.application_name
Environment = var.environment
}
}
# IAM policy for secrets access
resource "aws_iam_role_policy" "secrets_access" {
name = "${var.name_prefix}-secrets-access"
role = aws_iam_role.app_server.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"secretsmanager:GetSecretValue"
]
Resource = [
aws_secretsmanager_secret.app_secrets.arn
]
},
{
Effect = "Allow"
Action = [
"ssm:GetParameter",
"ssm:GetParameters",
"ssm:GetParametersByPath"
]
Resource = [
"arn:aws:ssm:${var.aws_region}:${data.aws_caller_identity.current.account_id}:parameter/${var.name_prefix}/config/*"
]
}
]
})
}
Security Automation
Automate security controls and compliance:
# CloudTrail for audit logging
resource "aws_cloudtrail" "main" {
name = "${var.name_prefix}-cloudtrail"
s3_bucket_name = aws_s3_bucket.cloudtrail_logs.bucket
event_selector {
read_write_type = "All"
include_management_events = true
data_resource {
type = "AWS::S3::Object"
values = ["arn:aws:s3:::${aws_s3_bucket.sensitive_data.bucket}/*"]
}
}
insight_selector {
insight_type = "ApiCallRateInsight"
}
tags = {
Purpose = "Security audit logging"
}
}
# Config for compliance monitoring
resource "aws_config_configuration_recorder" "main" {
name = "${var.name_prefix}-config-recorder"
role_arn = aws_iam_role.config.arn
recording_group {
all_supported = true
include_global_resource_types = true
}
}
resource "aws_config_delivery_channel" "main" {
name = "${var.name_prefix}-config-delivery"
s3_bucket_name = aws_s3_bucket.config_logs.bucket
}
# Config rules for compliance
resource "aws_config_config_rule" "root_access_key_check" {
name = "${var.name_prefix}-root-access-key-check"
source {
owner = "AWS"
source_identifier = "ROOT_ACCESS_KEY_CHECK"
}
depends_on = [aws_config_configuration_recorder.main]
}
resource "aws_config_config_rule" "encrypted_volumes" {
name = "${var.name_prefix}-encrypted-volumes"
source {
owner = "AWS"
source_identifier = "ENCRYPTED_VOLUMES"
}
depends_on = [aws_config_configuration_recorder.main]
}
KMS Key Management
Manage encryption keys for different services:
# Application-specific KMS key
resource "aws_kms_key" "app_key" {
description = "KMS key for ${var.application_name}"
deletion_window_in_days = 7
enable_key_rotation = true
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "Enable IAM User Permissions"
Effect = "Allow"
Principal = {
AWS = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"
}
Action = "kms:*"
Resource = "*"
},
{
Sid = "Allow use of the key"
Effect = "Allow"
Principal = {
AWS = [
aws_iam_role.app_server.arn
]
}
Action = [
"kms:Encrypt",
"kms:Decrypt",
"kms:ReEncrypt*",
"kms:GenerateDataKey*",
"kms:DescribeKey"
]
Resource = "*"
}
]
})
tags = {
Name = "${var.name_prefix}-app-key"
Application = var.application_name
}
}
resource "aws_kms_alias" "app_key" {
name = "alias/${var.name_prefix}-app-key"
target_key_id = aws_kms_key.app_key.key_id
}
# S3 bucket encryption with KMS
resource "aws_s3_bucket_server_side_encryption_configuration" "app_data" {
bucket = aws_s3_bucket.app_data.id
rule {
apply_server_side_encryption_by_default {
kms_master_key_id = aws_kms_key.app_key.arn
sse_algorithm = "aws:kms"
}
bucket_key_enabled = true
}
}
Security Group Automation
Create security groups with proper ingress/egress rules:
# Application security group with dynamic rules
resource "aws_security_group" "app" {
name_prefix = "${var.name_prefix}-app-"
vpc_id = var.vpc_id
description = "Security group for ${var.application_name}"
# Dynamic ingress rules
dynamic "ingress" {
for_each = var.ingress_rules
content {
description = ingress.value.description
from_port = ingress.value.from_port
to_port = ingress.value.to_port
protocol = ingress.value.protocol
cidr_blocks = ingress.value.cidr_blocks
security_groups = ingress.value.security_groups
}
}
# Allow all outbound traffic
egress {
description = "All outbound traffic"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.name_prefix}-app-sg"
}
}
# Database security group with restricted access
resource "aws_security_group" "database" {
name_prefix = "${var.name_prefix}-db-"
vpc_id = var.vpc_id
description = "Security group for database"
ingress {
description = "MySQL from application"
from_port = 3306
to_port = 3306
protocol = "tcp"
security_groups = [aws_security_group.app.id]
}
tags = {
Name = "${var.name_prefix}-db-sg"
}
}
IAM Access Analyzer
Use Access Analyzer to identify overly permissive policies:
resource "aws_accessanalyzer_analyzer" "main" {
analyzer_name = "${var.name_prefix}-access-analyzer"
type = "ACCOUNT"
tags = {
Environment = var.environment
Purpose = "IAM policy analysis"
}
}
# Archive findings that are expected
resource "aws_accessanalyzer_archive_rule" "ignore_public_s3" {
analyzer_name = aws_accessanalyzer_analyzer.main.analyzer_name
rule_name = "ignore-public-s3-buckets"
filter {
criteria = "resourceType"
eq = ["AWS::S3::Bucket"]
}
filter {
criteria = "isPublic"
eq = ["true"]
}
}
What’s Next
IAM and security form the foundation of AWS infrastructure protection, but managing multiple AWS accounts requires additional patterns for organization setup, cross-account access, and centralized governance.
In the next part, we’ll explore multi-account strategies using AWS Organizations, including account creation automation, cross-account role management, and centralized billing and compliance controls.