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.