Use Cases (更新: 2026/6/7)

AWS IAMポリシーの書き方と最小権限の作り方:Effect/Action/Resourceとロール入門

IAMポリシーのJSON構造(Effect/Action/Resource/Condition)と最小権限の設計、ロールとAssumeRoleの仕組み、広すぎる「*」など定番の落とし穴を、コピペで動く例つきで解説。

AWS IAMポリシーの書き方と最小権限の作り方:Effect/Action/Resourceとロール入門

「とりあえず動かしたいんで、権限は広めで」

そう言ってLambdaの実行ロールにs3:*を付けた日のことを、僕はまだ覚えています。デモは一発で通りました。気持ちよかった。問題は3週間後、別のレビューでそのロールが見つかったときです。サムネイルを作るだけのLambdaが、本番の全バケットを削除できる状態で放置されていました。

幸い、何も起きませんでした。でも「何も起きなかった」のは運がよかっただけです。IAMは、画面の上では小さなJSONに見えます。その小さなJSONが、S3の中身を消せるか・DynamoDBを読めるか・別のロールに化けられるかを、全部決めています。

この記事は、そのJSONをきちんと読み書きできるようになるための入門です。難しい言葉はその都度かみ砕きます。

この記事の要点

  • IAMポリシーは「Effect(許可/拒否)・Action(操作)・Resource(対象)・Condition(条件)」の4つで読む。ここさえ分かれば9割は読める。
  • 最小権限とは「必要な操作を、必要な対象にだけ許す」こと。Action: "*"Resource: "*"は、原則使わない。
  • ロールは「一時的に引き受ける役割」。誰が引き受けられるかを決めるのが信頼ポリシー(AssumeRole)、引き受けた後に何ができるかを決めるのが権限ポリシー。この2つは別物。
  • 落とし穴の定番は、広すぎる*・S3のARNの付け間違い・拒否のテスト漏れ・長期アクセスキーの放置の4つ。
  • 書いたポリシーは公開前にIAM Access Analyzerで検証できる。無料で文法とセキュリティ警告をチェックしてくれる。

そもそもIAMは「誰が・何に・何をしていいか」の表

IAM(Identity and Access Management)は、AWSの権限を管理する仕組みです。やっていることは、突き詰めると1枚の表に近いです。

言葉意味身近な例え
プリンシパル権限を使う側(ユーザー、サービス、ロール)建物に入る人
ポリシー「何をしていいか」を書いたルール入館証に書かれた立ち入り可能エリア
ロール一時的に引き受ける役割受付で借りる来客バッジ
アクション具体的な操作(s3:GetObjectなど)「3階の倉庫を開ける」
リソース操作の対象(特定のバケットなど)「3階の倉庫」そのもの

ポイントは、IAMはデフォルトで全部拒否だということです。何も書かなければ、誰も何もできません。だから僕らがやる作業は「禁止を足す」ことではなく、「必要な許可だけを足す」ことになります。最小権限という考え方が自然に出てくるのは、この仕組みのせいです。

ポリシーのJSONは4つの部品でできている

IAMポリシーの本体はJSONです。最初は呪文に見えますが、見る場所は決まっています。次の最小例を、4つの部品に分けて読んでみます。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ReadOneBucketObjects",
      "Effect": "Allow",
      "Action": ["s3:GetObject"],
      "Resource": "arn:aws:s3:::user-uploads-prod/incoming/*"
    }
  ]
}
  • Version: ポリシー言語のバージョン。2012-10-17を書いておけば良い、と覚えてしまって構いません(これが現行の最新です)。
  • Effect: Allow(許可)かDeny(拒否)。
  • Action: 許す操作。サービス名:操作名の形。ここでは「S3のオブジェクトを取得する」だけ。
  • Resource: 操作してよい対象をARN(AWSリソースの住所のようなID)で指定。ここではuser-uploads-prodバケットのincoming/配下だけ。

この例が言っているのは「user-uploads-prod/incoming/の中のファイルを読むことだけ許す」です。それ以外は全部できません。書き込みも、削除も、別のバケットも。最小権限とは、要するにこの4つを狭く埋める作業なんですね。

Conditionで「いつ許すか」を絞る

5つ目の部品としてConditionがあります。これは「どんな条件のときだけ許すか」を足すものです。例えば「暗号化して保存するときだけ書き込みを許す」はこう書けます。

{
  "Sid": "WriteEncryptedThumbnailsOnly",
  "Effect": "Allow",
  "Action": ["s3:PutObject"],
  "Resource": "arn:aws:s3:::user-thumbnails-prod/thumbnails/*",
  "Condition": {
    "StringEquals": {
      "s3:x-amz-server-side-encryption": "AES256"
    }
  }
}

Conditionは強力ですが、サービスごとに使える条件キーが違います。後で触れますが、ここを雑に書くと「動かない」か「ザルになる」のどちらかになりやすい場所です。

コピペで作る:Lambda実行ロールの最小権限ポリシー

部品が分かったら、現実的な1本を組み立てます。題材は「S3から画像を読み、サムネイルを別バケットへ保存し、結果をDynamoDBに書き、失敗したらSNSで通知するLambda」です。よくある構成ですが、雑にやるとs3:*が生えがちな構成でもあります。

下のJSONをpolicy-lambda-image-worker.jsonとして保存してください。アカウントID(123456789012)やバケット名は自分のものに置き換えます。ロググループはCDK等で先に作る前提なので、logs:CreateLogGroupはあえて渡していません。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ReadSourceImages",
      "Effect": "Allow",
      "Action": ["s3:GetObject"],
      "Resource": "arn:aws:s3:::user-uploads-prod/incoming/*"
    },
    {
      "Sid": "WriteThumbnails",
      "Effect": "Allow",
      "Action": ["s3:PutObject"],
      "Resource": "arn:aws:s3:::user-thumbnails-prod/thumbnails/*",
      "Condition": {
        "StringEquals": {
          "s3:x-amz-server-side-encryption": "AES256"
        }
      }
    },
    {
      "Sid": "WriteMetadata",
      "Effect": "Allow",
      "Action": ["dynamodb:PutItem", "dynamodb:UpdateItem"],
      "Resource": "arn:aws:dynamodb:ap-northeast-1:123456789012:table/image-metadata"
    },
    {
      "Sid": "PublishAlerts",
      "Effect": "Allow",
      "Action": ["sns:Publish"],
      "Resource": "arn:aws:sns:ap-northeast-1:123456789012:alert-topic"
    },
    {
      "Sid": "WriteLambdaLogs",
      "Effect": "Allow",
      "Action": ["logs:CreateLogStream", "logs:PutLogEvents"],
      "Resource": "arn:aws:logs:ap-northeast-1:123456789012:log-group:/aws/lambda/image-worker-prod:*"
    }
  ]
}

このポリシーの良いところは、ActionResourceも最初から狭いことです。「広く付けて後で削る」のではなく、「必要なものだけ足す」順番になっています。各StatementSid(ステートメントの名前)を付けておくと、後でレビューやエラー調査が一気に楽になります。

保存したら、公開前に文法とセキュリティ警告をチェックします。IAM Access Analyzerのvalidate-policyは無料で使えて、AWSのベストプラクティスに照らした警告まで出してくれます。

aws accessanalyzer validate-policy \
  --policy-document file://policy-lambda-image-worker.json \
  --policy-type IDENTITY_POLICY \
  --query "findings[?findingType=='ERROR' || findingType=='SECURITY_WARNING']"

findingTypeはERROR(壊れていて動かない)、SECURITY_WARNING(広すぎて危険)、WARNING(ベストプラクティス違反だが危険ではない)、SUGGESTION(改善提案)の4種類です。最低でもERRORSECURITY_WARNINGは残さないようにします。

ただし、Access Analyzerは万能の承認者ではありません。文法・ARN・アクション名・条件キー・セキュリティ警告を見てくれるだけで、「その操作が業務的に本当に必要か」「アカウントIDやバケット名が本番と一致しているか」は人間が見ます。機械チェックと人間レビューは役割が別、と割り切るのが安全です。

ロールとAssumeRole:来客バッジの仕組み

ここがIAMで一番つまずく場所です。ロールは「一時的に引き受ける役割」だと言いました。受付で借りる来客バッジを思い浮かべてください。バッジには2つの側面があります。

  1. 誰がこのバッジを借りられるか(受付のルール)= 信頼ポリシー
  2. このバッジで建物の中の何ができるか(バッジに書かれた権限)= 権限ポリシー

この2つは別の紙です。混ぜると、レビューが急に難しくなります。

「誰が引き受けられるか」を決めるのが信頼ポリシー(trust policy)で、その中でsts:AssumeRole系のアクションを許可します。例えばLambdaサービスにこのロールを使わせたいなら、信頼ポリシーはこうなります。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": { "Service": "lambda.amazonaws.com" },
      "Action": "sts:AssumeRole"
    }
  ]
}

Principalが「バッジを借りられる人(ここではLambdaというサービス)」です。一方、前のセクションで作った権限ポリシーは「バッジで何ができるか」を決めます。信頼ポリシー=入口、権限ポリシー=中身、と覚えてください。

人間や外部サービスにはOIDC+AssumeRoleを使う

人間ユーザーやCI(GitHub Actionsなど)に長期アクセスキーを持たせるのは、今は推奨されません。AWS公式のIAMベストプラクティスも、一時認証情報とロール利用を強く勧めています。GitHub Actionsからデプロイするなら、OIDC(外部の身元証明の仕組み)でロールを引き受ける構成にします。信頼ポリシーはこう書きます。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
        },
        "StringLike": {
          "token.actions.githubusercontent.com:sub": "repo:example-org/example-repo:ref:refs/heads/main"
        }
      }
    }
  ]
}

Conditionsubで「mainブランチからの実行だけ」と絞っているのがポイントです。ここを*にすると、フォークしたPRからでもロールを引き受けられてしまいます。実際にこれで事故った話はよく聞きます。

ユーザーとグループ:権限は1人ずつ付けない

人間のアカウント(IAMユーザー)に権限を付けるとき、1人ずつポリシーを貼っていくと、すぐ収拾がつかなくなります。代わりにグループを使います。

  • 「開発者グループ」「読み取り専用グループ」のようにグループを作る
  • グループにポリシーを付ける
  • ユーザーはグループに入れるだけ

こうすると、新しいメンバーが来ても「グループに入れる」で終わります。退職時も「グループから外す」で済みます。権限の付け外しを人ではなく役割で管理するわけです。

さらに大きな組織では、グループでも足りなくなります。そのときは権限の上限を決める**Permissions boundary(権限境界)**という仕組みもあります。これは「このユーザーには、たとえポリシーで許しても、ここまでしか与えない」という天井を設定するものです。最初から使う必要はありませんが、「許可を足す方向」だけでなく「上限を決める方向」の道具もある、と頭の隅に置いておくと後で効きます。

よくある落とし穴4つ(全部やらかしました)

正直に書きます。下の4つは、全部僕が一度はやった失敗です。

1. Action: "*" / Resource: "*" を「一時対応」で入れる。 一時対応は本番で恒久化します。冒頭のs3:*がまさにこれでした。どうしても緊急で広げるなら、期限付きチケット・削除予定日・CloudTrailでの事後確認をセットで残します。「あとで消す」は、メモがなければ消えません。

2. S3のARNを付ける場所を間違える。 これは超頻出です。s3:ListBucketはバケット本体(arn:aws:s3:::bucket-name)に付け、s3:GetObjects3:PutObjectはオブジェクト(arn:aws:s3:::bucket-name/prefix/*)に付けます。逆にすると、動かないか、広すぎる権限になります。

アクション付けるARN
s3:ListBucketバケット本体arn:aws:s3:::user-uploads-prod
s3:GetObject / s3:PutObjectオブジェクトarn:aws:s3:::user-uploads-prod/incoming/*

3. 拒否されるべき操作をテストしない。 s3:GetObjectが成功するかだけ確認して満足すると、s3:DeleteObjectまで通っていることに気づけません。権限テストは、通るべき操作(成功パス)と、通ってはいけない操作(失敗パス)の両方を確認します。「消せないこと」を確認して初めて、最小権限と言えます。

4. Conditionの意味を取り違える。 人間がコンソールから叩くAPIと、AWSサービスがロールで叩くAPIでは、条件キーの意味が違います。例えばaws:SourceIpで社内IPに絞ると安全に見えますが、Lambdaやサービス間呼び出しでは期待どおりに効かず、突然動かなくなることがあります。Conditionを足すときは「誰がこのAPIを呼ぶのか」を先に確認してください。

コードで固定して、レビューに残す

コンソールで手作業でロールを直すと、「誰がいつ何をなぜ変えたか」が残りません。次の人(半年後の自分を含む)が困ります。なので、ロールとポリシーはコードで管理して、Pull Requestでレビューできる状態にするのがおすすめです。AWS CDK(TypeScriptなどでインフラを書く道具)を使うと、前述のポリシーはこう表現できます。

import * as cdk from "aws-cdk-lib";
import { Stack, StackProps, aws_iam as iam, aws_logs as logs } from "aws-cdk-lib";
import { Construct } from "constructs";

export class ImageWorkerIamStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    const account = Stack.of(this).account;
    const region = Stack.of(this).region;

    // ロググループは先に作る(実行ロールにCreateLogGroupを渡さないため)
    new logs.LogGroup(this, "ImageWorkerLogGroup", {
      logGroupName: "/aws/lambda/image-worker-prod",
      retention: logs.RetentionDays.ONE_MONTH,
      removalPolicy: cdk.RemovalPolicy.RETAIN,
    });

    // 信頼ポリシー=「誰が引き受けられるか」をassumedByで指定
    const role = new iam.Role(this, "ImageWorkerRole", {
      assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"),
      description: "Execution role for image-worker-prod Lambda",
    });

    // ここから先は権限ポリシー=「引き受けた後に何ができるか」
    role.addToPolicy(new iam.PolicyStatement({
      sid: "ReadSourceImages",
      actions: ["s3:GetObject"],
      resources: ["arn:aws:s3:::user-uploads-prod/incoming/*"],
    }));

    role.addToPolicy(new iam.PolicyStatement({
      sid: "WriteThumbnails",
      actions: ["s3:PutObject"],
      resources: ["arn:aws:s3:::user-thumbnails-prod/thumbnails/*"],
      conditions: {
        StringEquals: { "s3:x-amz-server-side-encryption": "AES256" },
      },
    }));

    role.addToPolicy(new iam.PolicyStatement({
      sid: "WriteMetadataAndAlerts",
      actions: ["dynamodb:PutItem", "dynamodb:UpdateItem", "sns:Publish"],
      resources: [
        `arn:aws:dynamodb:${region}:${account}:table/image-metadata`,
        `arn:aws:sns:${region}:${account}:alert-topic`,
      ],
    }));

    role.addToPolicy(new iam.PolicyStatement({
      sid: "WriteLambdaLogs",
      actions: ["logs:CreateLogStream", "logs:PutLogEvents"],
      resources: [
        `arn:aws:logs:${region}:${account}:log-group:/aws/lambda/image-worker-prod:*`,
      ],
    }));
  }
}

アカウントIDとリージョンはStack.of(this)から取るので、ハードコードしません。これで環境ごとの貼り間違いが減ります。CDKのsynthdiffdeployの流れや差分の読み方は、AWS CDK入門:CloudFormationとの違いで詳しく扱っています。

レビューのときに貼るチェックリスト

Pull Requestでポリシーを見るときは、毎回このリストをコメントに貼ると見落としが減ります。

  • このロールを使う実行主体は1つに絞れているか
  • Actionに削除(*Delete*)・IAM操作・全サービス操作(*)が混ざっていないか
  • Resourceが本番のARN・prefix・リージョン・アカウントIDに固定されているか
  • ConditionはそのAPIの呼ばれ方(人間かサービスか)に合っているか
  • Access AnalyzerのERRORSECURITY_WARNINGが残っていないか
  • 成功パスだけでなく、拒否されるべき操作(失敗パス)もテストしたか
  • 30日後に未使用権限を棚卸しするチケットを作ったか

IAMは「一度作って終わり」ではありません。新機能、障害対応、外部連携、担当者交代のたびに権限は太っていきます。定期的な棚卸しは地味な作業ですが、ここをサボると、また誰かのLambdaにs3:*が生えます。

よくある質問

Q. AllowDeny、両方書いたらどっちが勝つ? Denyが必ず勝ちます。明示的なDenyがあると、他のどんなAllowがあっても拒否されます。なので「絶対にやらせたくない操作」を明示的Denyで禁じるのは有効な手です。

Q. AWS管理ポリシー(AmazonS3FullAccessなど)を使ってはダメ? 最初の学習や検証では便利ですが、本番では広すぎることが多いです。FullAccess系は削除権限まで含むので、最小権限とは逆方向です。慣れてきたら、自分で必要な操作だけ書いたポリシーに置き換えるのがおすすめです。

Q. ポリシーが効かない。どう調べる? まずAccess Analyzerのvalidate-policyで文法とARNを確認します。それでも動かない場合は、IAMの「ポリシーシミュレーター」や、実際のAPI呼び出しに対するCloudTrailのログで「どの操作が拒否されたか」を確認します。ログの監査設計はCloudWatch Logs Insightsとアラーム設計が参考になります。

Q. ユーザーにアクセスキーを発行するのは普通のこと? 人間に対しては、今は非推奨です。長期アクセスキーは漏れたら使われ続けます。人間はフェデレーション(IAM Identity Centerなど)、サービスやCIはロール+一時認証情報、と覚えてください。

Q. AssumeRoleとAssumeRoleWithWebIdentityの違いは? どちらも「ロールを引き受けて一時認証情報を得る」操作です。AssumeRoleWithWebIdentityは、GitHub ActionsのOIDCのように外部の身元証明(Webアイデンティティ)を使う場合に使います。AWS内のサービスやユーザーからなら通常のAssumeRoleです。

実際に試した結果

冒頭のs3:*事故以来、僕がIAMで変えたことは2つだけです。

ひとつは、ポリシーを書く前に**「禁止したい操作」を先に決める**こと。DeleteObjectは要らない、IAM操作は要らない、と先に宣言してからAllowを組み立てると、広い*が混ざる頻度が目に見えて下がりました。許可を足すより、要らないものを先に潰すほうが速いんですね。

もうひとつは、公開前にAccess Analyzerと拒否テストを必ず通すこと。これを入れてから、レビューが「たぶん大丈夫」ではなく「警告ゼロ・失敗パス確認済み」という証拠つきで進むようになりました。安心感ではなく証拠で語れると、チームのレビューも一気に速くなります。

IAMは派手な技術ではありません。でも、ここを雑にすると、上に何を積んでも全部崩れます。まずは今日のEffect/Action/Resourceの読み方と、信頼ポリシーと権限ポリシーを分ける感覚だけ持って帰ってください。それだけで、事故の半分は防げます。

関連して、Lambda側の実装と実行ロールの設計はAWS Lambda実装の勘所、S3のprefix設計はAWS S3入門、DynamoDBの権限とテーブル設計はDynamoDBはアクセスパターンから設計する、AIに作業を任せる前の安全設定全般は初心者がまず守る安全対策が地続きです。チームでIAMやCDK、CI権限のレビュー手順を整えたいなら研修・導入相談、繰り返し使うレビュー文面やテンプレートは教材一覧から選べます。

公式の一次情報は、必ずここで確認してください。Security best practices in IAM(AWS公式)

#AWS #IAM #最小権限 #セキュリティ #ポリシー #ロール
無料

無料PDF: Claude Code はじめてのチートシート

まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。

スパムは送りません。登録情報は厳重に管理します。

Claude Codeを仕事で使える形にしませんか?

まず無料PDFで基本を固め、繰り返し使う作業はGumroad教材へ、チーム導入や権限設計は導入相談へ進めます。

Masa

この記事を書いた人

Masa

Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。

PR

関連書籍・参考図書

この記事のテーマに関連する書籍を楽天ブックスで探せます。

※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。