Imported from engineers-hub-ltd-in-house-project/eh-skills (
skills/aws-infrastructure/iam-security/SKILL.md). Install upstream withnpx skills add engineers-hub-ltd-in-house-project/eh-skills --skill iam-security. Copyright stays with the author (Apache-2.0).
IAM セキュリティ設計パターン
このスキルを使うタイミング
- AWS リソースへのアクセス制御ポリシーを設計するとき
- IAM ロールを作成しサービスやユーザーに権限を付与するとき
- クロスアカウントアクセスを構成するとき
- CI/CD パイプラインの実行ロールを設計するとき
- 条件キーで細粒度のアクセス制御を実装するとき
- 権限境界(Permissions Boundary)で権限の上限を設定するとき
基本パターン
最小権限ポリシー設計
# アプリケーション用 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 タスク実行ロール
# タスク実行ロール(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}/*",
]
}
]
})
}
クロスアカウントアクセス
# 本番アカウントの 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}/*",
]
}
]
})
}
// --- 開発アカウントのアプリケーションコード ---
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)
# 開発者が作成できるロールの権限上限を定義
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)
# 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"
}
}
}
]
})
}
必須ルール
- 最小権限の原則を徹底 --
*ワイルドカードのリソース指定は最終手段。具体的な ARN で制限する - IAM ユーザーよりロールを優先 -- 長期的なアクセスキーは避け、AssumeRole を使用する
- 条件キーで追加制限 --
aws:SourceAccount,aws:SourceArn,aws:ResourceTag等で制御を強化する - ExternalId を使用 -- クロスアカウントアクセスでは混乱した代理人問題を防止する
- Permissions Boundary を活用 -- 開発者に IAM ロール作成を許可する場合は権限の上限を設定する
- OIDC を使用 -- CI/CD パイプラインでは長期クレデンシャルではなく OIDC 連携を採用する
- Sid を付与 -- ポリシーの各 Statement に意図を示す Sid を必ず設定する
アンチパターン
Action: "*"とResource: "*"の組み合わせ(管理者ポリシー以外では禁止)- IAM ユーザーにアクセスキーを発行して永続化する(ローテーション漏れのリスク)
- 複数サービスで同一ロールを共有する(権限が肥大化し最小権限を維持できない)
- インラインポリシーを多用する(管理ポリシーの方が再利用・監査が容易)
NotAction/NotResourceの安易な使用(意図しない許可が生まれやすい)- 本番環境で
AdministratorAccessマネージドポリシーを使用する - クロスアカウントアクセスで ExternalId を省略する(混乱した代理人攻撃のリスク)
テスト戦略
IAM セキュリティのテストでは以下のケースを検証する:
- 最小権限検証: 許可されたアクションのみ実行可能であること(
iam:SimulatePrincipalPolicyを使用) - 拒否検証: 明示的に拒否されたアクションが実行できないこと
- クロスアカウント検証: 正しい ExternalId でのみ AssumeRole が成功すること
- Permissions Boundary 検証: 境界を超える権限が付与されないこと
- 条件キー検証: 条件を満たさないリクエストが拒否されること
- OIDC 検証: 指定リポジトリ・ブランチからのみ AssumeRole が成功すること
- IAM Access Analyzer: 未使用の権限やパブリックアクセスを定期的にスキャンする