---
name: iam-security
description: |
  AWS IAM（Identity and Access Management）のセキュリティ設計パターン。最小権限の原則に基づく
  ポリシー設計、IAMロールの構成、クロスアカウントアクセス、条件キーによるアクセス制御、
  サービスリンクロール、権限境界（Permissions Boundary）を網羅する。
  セキュアなAWS環境構築において最も重要な基盤であり、全てのAWSリソース設計の前提となる。
license: Apache-2.0
metadata:
  author: engineers-hub
  version: "1.0.0"
  category: aws-infrastructure
  tags: [iam, security, policy, role, least-privilege, cross-account, permissions-boundary]
  dependencies: []
---

# IAM セキュリティ設計パターン

## このスキルを使うタイミング

- AWS リソースへのアクセス制御ポリシーを設計するとき
- IAM ロールを作成しサービスやユーザーに権限を付与するとき
- クロスアカウントアクセスを構成するとき
- CI/CD パイプラインの実行ロールを設計するとき
- 条件キーで細粒度のアクセス制御を実装するとき
- 権限境界（Permissions Boundary）で権限の上限を設定するとき

## 基本パターン

### 最小権限ポリシー設計

```hcl
# アプリケーション用 IAM ロール（ECS タスクロールの例）
resource "aws_iam_role" "app_task" {
  name = "${var.project}-${var.environment}-app-task-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Principal = {
          Service = "ecs-tasks.amazonaws.com"
        }
        Action = "sts:AssumeRole"
        Condition = {
          ArnLike = {
            "aws:SourceArn" = "arn:aws:ecs:${var.region}:${var.account_id}:*"
          }
          StringEquals = {
            "aws:SourceAccount" = var.account_id
          }
        }
      }
    ]
  })

  tags = {
    Name        = "${var.project}-${var.environment}-app-task-role"
    Environment = var.environment
  }
}

# S3 バケットへのアクセスポリシー（最小権限）
resource "aws_iam_role_policy" "app_s3_access" {
  name = "${var.project}-${var.environment}-s3-access"
  role = aws_iam_role.app_task.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid    = "ReadFromInputBucket"
        Effect = "Allow"
        Action = [
          "s3:GetObject",
          "s3:ListBucket",
        ]
        Resource = [
          aws_s3_bucket.input.arn,
          "${aws_s3_bucket.input.arn}/*",
        ]
      },
      {
        Sid    = "WriteToOutputBucket"
        Effect = "Allow"
        Action = [
          "s3:PutObject",
          "s3:DeleteObject",
        ]
        Resource = [
          "${aws_s3_bucket.output.arn}/*",
        ]
      }
    ]
  })
}

# Secrets Manager からのシークレット取得
resource "aws_iam_role_policy" "app_secrets" {
  name = "${var.project}-${var.environment}-secrets-access"
  role = aws_iam_role.app_task.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid    = "GetSecrets"
        Effect = "Allow"
        Action = [
          "secretsmanager:GetSecretValue",
        ]
        Resource = [
          "arn:aws:secretsmanager:${var.region}:${var.account_id}:secret:${var.project}/${var.environment}/*",
        ]
        Condition = {
          StringEquals = {
            "aws:ResourceTag/Environment" = var.environment
          }
        }
      }
    ]
  })
}
```

### ECS タスク実行ロール

```hcl
# タスク実行ロール（ECR からのイメージ取得、CloudWatch Logs への書き込み）
resource "aws_iam_role" "ecs_execution" {
  name = "${var.project}-${var.environment}-ecs-execution-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Principal = {
          Service = "ecs-tasks.amazonaws.com"
        }
        Action = "sts:AssumeRole"
      }
    ]
  })
}

resource "aws_iam_role_policy_attachment" "ecs_execution_base" {
  role       = aws_iam_role.ecs_execution.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}

# Secrets Manager / SSM Parameter Store からの値取得（タスク定義の secrets で参照）
resource "aws_iam_role_policy" "ecs_execution_secrets" {
  name = "${var.project}-${var.environment}-execution-secrets"
  role = aws_iam_role.ecs_execution.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid    = "GetSecretsForTaskDefinition"
        Effect = "Allow"
        Action = [
          "secretsmanager:GetSecretValue",
        ]
        Resource = [
          "arn:aws:secretsmanager:${var.region}:${var.account_id}:secret:${var.project}/${var.environment}/*",
        ]
      },
      {
        Sid    = "GetSSMParameters"
        Effect = "Allow"
        Action = [
          "ssm:GetParameters",
        ]
        Resource = [
          "arn:aws:ssm:${var.region}:${var.account_id}:parameter/${var.project}/${var.environment}/*",
        ]
      }
    ]
  })
}
```

### クロスアカウントアクセス

```hcl
# 本番アカウントの S3 バケットへ開発アカウントからアクセスするパターン

# --- 本番アカウント側 ---
resource "aws_iam_role" "cross_account_reader" {
  name = "${var.project}-cross-account-reader"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Principal = {
          AWS = "arn:aws:iam::${var.dev_account_id}:root"
        }
        Action = "sts:AssumeRole"
        Condition = {
          StringEquals = {
            "sts:ExternalId" = var.external_id
          }
        }
      }
    ]
  })

  max_session_duration = 3600
}

resource "aws_iam_role_policy" "cross_account_reader_policy" {
  name = "read-only-access"
  role = aws_iam_role.cross_account_reader.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "s3:GetObject",
          "s3:ListBucket",
        ]
        Resource = [
          aws_s3_bucket.shared_data.arn,
          "${aws_s3_bucket.shared_data.arn}/*",
        ]
      }
    ]
  })
}
```

```typescript
// --- 開発アカウントのアプリケーションコード ---
import { STSClient, AssumeRoleCommand } from "@aws-sdk/client-sts";
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";

async function getS3ClientForProductionAccount(): Promise<S3Client> {
  const stsClient = new STSClient({ region: process.env.AWS_REGION });

  const assumeRoleResponse = await stsClient.send(
    new AssumeRoleCommand({
      RoleArn: `arn:aws:iam::${process.env.PRODUCTION_ACCOUNT_ID}:role/${process.env.PROJECT}-cross-account-reader`,
      RoleSessionName: "dev-account-reader",
      ExternalId: process.env.EXTERNAL_ID,
      DurationSeconds: 3600,
    }),
  );

  const credentials = assumeRoleResponse.Credentials;
  if (!credentials) {
    throw new Error("Failed to assume cross-account role");
  }

  return new S3Client({
    region: process.env.AWS_REGION,
    credentials: {
      accessKeyId: credentials.AccessKeyId!,
      secretAccessKey: credentials.SecretAccessKey!,
      sessionToken: credentials.SessionToken!,
    },
  });
}
```

### 権限境界（Permissions Boundary）

```hcl
# 開発者が作成できるロールの権限上限を定義
resource "aws_iam_policy" "developer_boundary" {
  name = "${var.project}-developer-permissions-boundary"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid    = "AllowCommonServices"
        Effect = "Allow"
        Action = [
          "s3:*",
          "dynamodb:*",
          "sqs:*",
          "sns:*",
          "lambda:*",
          "logs:*",
          "cloudwatch:*",
          "ecr:*",
          "ecs:*",
          "secretsmanager:GetSecretValue",
        ]
        Resource = "*"
      },
      {
        Sid    = "DenyIAMEscalation"
        Effect = "Deny"
        Action = [
          "iam:CreateUser",
          "iam:CreateAccessKey",
          "iam:AttachUserPolicy",
          "iam:PutUserPolicy",
          "organizations:*",
          "account:*",
        ]
        Resource = "*"
      },
      {
        Sid      = "DenyCriticalResourceDeletion"
        Effect   = "Deny"
        Action   = [
          "rds:DeleteDBInstance",
          "rds:DeleteDBCluster",
          "s3:DeleteBucket",
        ]
        Resource = "*"
        Condition = {
          StringEquals = {
            "aws:ResourceTag/Protected" = "true"
          }
        }
      }
    ]
  })
}

# 開発者ロール作成時に Permissions Boundary を強制
resource "aws_iam_role" "developer" {
  name                 = "${var.project}-developer-role"
  permissions_boundary = aws_iam_policy.developer_boundary.arn

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Principal = {
          Federated = "arn:aws:iam::${var.account_id}:saml-provider/${var.idp_name}"
        }
        Action = "sts:AssumeRoleWithSAML"
        Condition = {
          StringEquals = {
            "SAML:aud" = "https://signin.aws.amazon.com/saml"
          }
        }
      }
    ]
  })
}
```

### CI/CD パイプラインロール（GitHub Actions OIDC）

```hcl
# GitHub Actions から OIDC で AssumeRole
resource "aws_iam_openid_connect_provider" "github" {
  url             = "https://token.actions.githubusercontent.com"
  client_id_list  = ["sts.amazonaws.com"]
  thumbprint_list = ["6938fd4d98bab03faadb97b34396831e3780aea1"]
}

resource "aws_iam_role" "github_actions" {
  name = "${var.project}-github-actions-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Principal = {
          Federated = aws_iam_openid_connect_provider.github.arn
        }
        Action = "sts:AssumeRoleWithWebIdentity"
        Condition = {
          StringEquals = {
            "token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
          }
          StringLike = {
            "token.actions.githubusercontent.com:sub" = "repo:${var.github_org}/${var.github_repo}:ref:refs/heads/main"
          }
        }
      }
    ]
  })
}
```

## 必須ルール

1. **最小権限の原則を徹底** -- `*` ワイルドカードのリソース指定は最終手段。具体的な ARN で制限する
2. **IAM ユーザーよりロールを優先** -- 長期的なアクセスキーは避け、AssumeRole を使用する
3. **条件キーで追加制限** -- `aws:SourceAccount`, `aws:SourceArn`, `aws:ResourceTag` 等で制御を強化する
4. **ExternalId を使用** -- クロスアカウントアクセスでは混乱した代理人問題を防止する
5. **Permissions Boundary を活用** -- 開発者に IAM ロール作成を許可する場合は権限の上限を設定する
6. **OIDC を使用** -- CI/CD パイプラインでは長期クレデンシャルではなく OIDC 連携を採用する
7. **Sid を付与** -- ポリシーの各 Statement に意図を示す Sid を必ず設定する

## アンチパターン

- `Action: "*"` と `Resource: "*"` の組み合わせ（管理者ポリシー以外では禁止）
- IAM ユーザーにアクセスキーを発行して永続化する（ローテーション漏れのリスク）
- 複数サービスで同一ロールを共有する（権限が肥大化し最小権限を維持できない）
- インラインポリシーを多用する（管理ポリシーの方が再利用・監査が容易）
- `NotAction` / `NotResource` の安易な使用（意図しない許可が生まれやすい）
- 本番環境で `AdministratorAccess` マネージドポリシーを使用する
- クロスアカウントアクセスで ExternalId を省略する（混乱した代理人攻撃のリスク）

## テスト戦略

IAM セキュリティのテストでは以下のケースを検証する:

1. **最小権限検証**: 許可されたアクションのみ実行可能であること（`iam:SimulatePrincipalPolicy` を使用）
2. **拒否検証**: 明示的に拒否されたアクションが実行できないこと
3. **クロスアカウント検証**: 正しい ExternalId でのみ AssumeRole が成功すること
4. **Permissions Boundary 検証**: 境界を超える権限が付与されないこと
5. **条件キー検証**: 条件を満たさないリクエストが拒否されること
6. **OIDC 検証**: 指定リポジトリ・ブランチからのみ AssumeRole が成功すること
7. **IAM Access Analyzer**: 未使用の権限やパブリックアクセスを定期的にスキャンする
