AWS IAM 最小権限の法則:実務で事故を防ぐポリシー設計と動的生成のパターン
「とりあえずFullAccess」はNG。最小権限の原則に基づいたIAMポリシー設計の実践パターンを、LP構築・CI/CD・Lambda実行ロールの実例で解説。
はじめに:「動いているから良い」が事故の温床になる
IAMポリシーの設定ミスが引き起こす事故には、大きく2つの類型がある。
事故の類型:
類型A: 過剰権限による情報漏洩・破壊
────────────────────────────────────
Lambda に AdministratorAccess を付与
→ Lambdaの脆弱性を突かれ、攻撃者がAWS環境全体を掌握
→ S3バケットの全データ取得・EC2の大量起動・他リソース破壊
類型B: 権限不足による運用事故
────────────────────────────────────
デプロイ用ロールの権限を絞りすぎて
→ 深夜リリース中に「AccessDenied」が出てデプロイ停止
→ 緊急で AdministratorAccess を付与して解決
→ そのまま外すのを忘れる(よくある)
どちらも「権限設計を後回しにした」結果だ。「最小権限」は絞ることが目的ではなく、「必要な権限を正確に把握すること」が本質だ。
Part 1:IAMポリシー設計の3原則
実務で使える判断軸として整理する。
原則1:アイデンティティではなくユースケースで考える
❌ よくある考え方:
「開発者用ロール」「本番用ロール」という人物軸での設計
→ 一人の開発者が S3・EC2・Lambda・RDS に触れる巨大ロールが生まれる
✅ 推奨する考え方:
「S3バケットXにファイルをアップロードする」
「Lambda関数Yを実行してCloudWatchにログを書く」
という動詞+リソース軸での設計
→ ユースケース単位の小さなロールが生まれ、組み合わせで使う
原則2:Action を動詞レベルで絞る
権限の粒度の比較:
NG: "s3:*"
△: "s3:GetObject", "s3:PutObject", "s3:DeleteObject", "s3:ListBucket"
✅: 「このLambdaはオブジェクトを読むだけ」なら "s3:GetObject" のみ
実際の確認コマンド:
# CloudTrailでLambdaが実際に呼んだAPIを確認する
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=Username,AttributeValue=<role-name> \
--query 'Events[].{API:EventName, Time:EventTime}' \
--output table
原則3:Resource を ARN レベルで絞る
// ❌ NG: アカウント全体のS3を許可
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": "*"
}
// ✅ 推奨: 特定バケットの特定パスのみ
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": [
"arn:aws:s3:::my-lp-assets-prd/uploads/*",
"arn:aws:s3:::my-lp-assets-prd/public/*"
]
}
Part 2:実務ロール別のポリシー設計パターン
パターンA:CI/CDデプロイロール(S3 + CloudFront)
LP案件でよく使う構成。S3へのファイル同期とCloudFrontのキャッシュ無効化だけを許可する。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "S3BucketLevelAccess",
"Effect": "Allow",
"Action": ["s3:ListBucket", "s3:GetBucketLocation"],
"Resource": "arn:aws:s3:::my-lp-assets-prd"
},
{
"Sid": "S3ObjectAccess",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject"
],
"Resource": "arn:aws:s3:::my-lp-assets-prd/*"
},
{
"Sid": "CloudFrontInvalidation",
"Effect": "Allow",
"Action": "cloudfront:CreateInvalidation",
"Resource": "arn:aws:cloudfront::123456789012:distribution/ABCDEFGHIJKLMN"
}
]
}
ポイント: s3:ListBucket は Resource にバケット ARN(末尾 /* なし)を指定する。オブジェクト操作は /* が必要。この違いを間違えると AccessDenied になる。
パターンB:Lambda実行ロール(最小構成)
Lambda に付与するロールは「このLambdaが実際に呼ぶサービス」だけに絞る。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "CloudWatchLogs",
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:ap-northeast-1:123456789012:log-group:/aws/lambda/my-function:*"
},
{
"Sid": "SESEmailSend",
"Effect": "Allow",
"Action": "ses:SendEmail",
"Resource": "*",
"Condition": {
"StringEquals": {
"ses:FromAddress": "noreply@example.com"
}
}
},
{
"Sid": "DynamoDBAccess",
"Effect": "Allow",
"Action": [
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:UpdateItem"
],
"Resource": "arn:aws:dynamodb:ap-northeast-1:123456789012:table/contact-form-entries"
}
]
}
ポイント: SES に Condition で送信元アドレスを固定している。Lambdaが侵害されても、攻撃者は noreply@example.com からしかメールを送れない。
パターンC:Terraform apply ロール(動的な権限絞り込み)
Terraform apply には「作成するリソースへの権限」が必要だが、それ以外は不要だ。管理対象リソースを明示的にリストアップしてポリシーを構成する。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "S3InfraManagement",
"Effect": "Allow",
"Action": [
"s3:CreateBucket", "s3:DeleteBucket",
"s3:GetBucketPolicy", "s3:PutBucketPolicy",
"s3:GetBucketVersioning", "s3:PutBucketVersioning",
"s3:GetBucketWebsite", "s3:PutBucketWebsite"
],
"Resource": "arn:aws:s3:::my-lp-*"
},
{
"Sid": "CloudFrontManagement",
"Effect": "Allow",
"Action": [
"cloudfront:CreateDistribution",
"cloudfront:GetDistribution",
"cloudfront:UpdateDistribution",
"cloudfront:TagResource"
],
"Resource": "*"
},
{
"Sid": "TFStateAccess",
"Effect": "Allow",
"Action": [
"s3:GetObject", "s3:PutObject",
"dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:DeleteItem"
],
"Resource": [
"arn:aws:s3:::my-lp-tfstate-prd/*",
"arn:aws:dynamodb:ap-northeast-1:123456789012:table/my-lp-tflock-prd"
]
},
{
"Sid": "IAMRoleManagement",
"Effect": "Allow",
"Action": [
"iam:CreateRole", "iam:DeleteRole",
"iam:GetRole", "iam:PassRole",
"iam:AttachRolePolicy", "iam:DetachRolePolicy",
"iam:PutRolePolicy", "iam:DeleteRolePolicy"
],
"Resource": "arn:aws:iam::123456789012:role/my-lp-*"
}
]
}
ポイント: Resource のパターンに my-lp-* というプレフィックスを使っている。プロジェクト命名規則を統一することで、ワイルドカードでも「このプロジェクトのリソースのみ」という範囲を保てる。
Part 3:Terraformによる動的ポリシー生成
ポリシーにARNをハードコードすると、リソースが再作成された際に手動更新が発生する。Terraformの data ブロックと aws_iam_policy_document を組み合わせると、ARNを動的に参照できる。
# リソースのARNを動的に取得
data "aws_s3_bucket" "lp_assets" {
bucket = aws_s3_bucket.lp.id
}
data "aws_cloudfront_distribution" "lp" {
id = aws_cloudfront_distribution.lp.id
}
data "aws_caller_identity" "current" {}
data "aws_region" "current" {}
# ポリシードキュメントをコードで生成
data "aws_iam_policy_document" "deploy_policy" {
statement {
sid = "S3BucketAccess"
effect = "Allow"
actions = ["s3:ListBucket", "s3:GetBucketLocation"]
# ハードコードせず、リソース参照で ARN を取得
resources = [data.aws_s3_bucket.lp_assets.arn]
}
statement {
sid = "S3ObjectAccess"
effect = "Allow"
actions = ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"]
resources = ["${data.aws_s3_bucket.lp_assets.arn}/*"]
}
statement {
sid = "CloudFrontInvalidation"
effect = "Allow"
actions = ["cloudfront:CreateInvalidation"]
resources = [data.aws_cloudfront_distribution.lp.arn]
}
statement {
sid = "CloudWatchLogs"
effect = "Allow"
actions = [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
]
resources = [
"arn:aws:logs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:log-group:/aws/lambda/${var.function_name}:*"
]
}
}
# 生成したポリシードキュメントをロールに付与
resource "aws_iam_role_policy" "deploy" {
name = "deploy-inline-policy"
role = aws_iam_role.deploy.id
policy = data.aws_iam_policy_document.deploy_policy.json
}
動的生成の利点:
ハードコード方式の問題:
S3バケットを作り直す
→ ARN変更
→ ポリシーを手動更新しないと権限が壊れる
→ 人間のミスが入る余地
動的参照方式:
S3バケットを作り直す
→ terraform apply が ARN を自動で引き直す
→ ポリシーも自動で更新される
→ コードが常に正典
Part 4:Permission Boundary でガードレールを引く
「Terraform apply ロールが新しいIAMロールを作れる」状態では、そのロールが自分より強い権限を持つロールを作れてしまう(権限昇格攻撃)。Permission Boundary はこれを防ぐ仕組みだ。
# 最大でも与えられる権限の上限を定義する Boundary Policy
resource "aws_iam_policy" "permission_boundary" {
name = "lp-project-boundary"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
# この境界内では S3・CloudFront・Lambda・CloudWatch のみ許可
Effect = "Allow"
Action = [
"s3:*",
"cloudfront:*",
"lambda:*",
"logs:*",
"ses:SendEmail"
]
Resource = "*"
},
{
# IAM の自己権限以上の操作は明示的に拒否
Effect = "Deny"
Action = [
"iam:CreateUser",
"iam:AttachUserPolicy",
"organizations:*"
]
Resource = "*"
}
]
})
}
# 新しいロールを作成する際に Boundary を強制付与
resource "aws_iam_role" "lambda_exec" {
name = "my-lp-lambda-exec"
assume_role_policy = data.aws_iam_policy_document.lambda_trust.json
permissions_boundary = aws_iam_policy.permission_boundary.arn
}
Permission Boundary の効果:
攻撃者が Lambda の脆弱性を突いて
IAM ロールを新規作成しようとしても:
境界ポリシーで "iam:CreateUser" が Deny されているため
どんな操作をしても IAM ユーザー/ロールの作成は不可能
→ 爆発半径(Blast Radius)を S3・Lambda の範囲に封じ込められる
Part 5:IAM Access Analyzer で継続的に検証する
ポリシーを設計しても、運用中に「使われていない権限」が蓄積する。IAM Access Analyzerはこれを検出する。
# 未使用の権限を検出(AWS CLI)
aws iam generate-service-last-accessed-details \
--arn arn:aws:iam::123456789012:role/my-lp-deploy-role
# 90日以上使われていないアクションを確認
aws iam get-service-last-accessed-details \
--job-id <job-id> \
--query 'ServicesLastAccessed[?LastAuthenticated==`null` || LastAuthenticated<=`2024-10-01T00:00:00`].{Service:ServiceName, LastUsed:LastAuthenticated}' \
--output table
継続的な権限レビューのフロー:
1. 初期設計: 推測ではなくユースケースから設計する
2. 初回デプロイ後 30日: CloudTrail で実際に呼ばれた API を確認
3. 初回デプロイ後 90日: Access Analyzer で未使用権限を洗い出す
4. 四半期ごと: 使われていない権限を削除するPRを作成
5. メンバー離脱時: そのメンバーが使っていたロールの権限を見直す
Terraform管理下であれば、権限の削除もPRとして履歴に残る。「なぜこの権限を削除したか」がコードの変更履歴として追跡できる。
Conclusion:「設定ではなく設計」としてIAMを捉える
IAMポリシーは「AWSを使うための設定作業」ではなく、「システムの信頼境界を定義する設計作業」だ。
IAMを「設定」として扱う場合:
→ アクセスできないエラーが出たら権限を追加する
→ 気づけばAdministratorAccessが乱立する
→ 誰がどのリソースに触れるか把握できなくなる
IAMを「設計」として扱う場合:
→ ユースケースを列挙してから権限を導出する
→ コードで管理するので変更履歴が残る
→ 未使用権限を定期的に棚卸しできる
| 観点 | 場当たり的な権限付与 | 設計ベースの最小権限 |
|---|---|---|
| 初期コスト | 低い | 高い(ユースケース列挙が必要) |
| 運用コスト | 高い(謎のAccessDeniedが頻発) | 低い |
| 事故時の爆発半径 | 大きい | Permission Boundaryで限定可能 |
| 権限の把握 | 不可能 | コードとCloudTrailで追跡可能 |
| メンバー変更時 | 危険(権限が残存) | ロール単位で剥奪・追加 |
「動いているから問題ない」は、IAMにおいては「まだ問題が顕在化していない」と同義だ。
この記事をシェア