IAM: User、Role、Policy の正しい使い方

1. 概要

IAM(Identity and Access Management)はAWSの基盤となるサービスで、「誰が何に対して何ができるのか」を決定します。多くのチームは最初から誤った使い方をしてしまいます。すべてのリソースに対するadmin権限を持つuserを作成したり、*:*というpolicyを適用したり、userとroleを混同してしまったりします。このガイドでは、user、role、policyの3つの概念を明確に区別し、正しいJSON policy文の書き方、least-privilege原則を適用する方法、およびJava SDKを使用してroleのassumeをテストする方法を説明します。

このガイドを読み終わると、シンプルで安全なIAM構成を自信を持って設計でき、すべてのサービスに対する「god-mode」権限を避けることができます。

2. 3つのコア要素

IAMは3つの中心的な概念で構成されています:

  • User:特定の人物またはアプリケーション用の識別子で、長期的な認証情報(パスワード、access key)を持ちます。
  • Role:一時的な識別子で、固定の認証情報を持たず、user、service、または他のアカウントによって「assume」(引き継ぎ)されます。
  • Policy:user、role、group、またはリソースにアタッチされる、権限(allow/deny)を記述したJSON形式のドキュメントです。

注意:Roleは「パスワードを持たないuser」ではありません。Roleは(STSを通じて)一時的な認証情報を生成し、userは(rotate操作を行うまで)固定の認証情報を使用します。

3. User:使用する場合と使用しない場合

Userが適切な場合:

  • Consoleにログインして手動で作業する必要がある人物。
  • Roleを使用できない避けられない状況(稀な場合です。例えば、AWS外のレガシーシステムが固定のaccess keyを必要とする場合)。

Userが不適切な場合:

  • EC2、Lambda、ECSで実行されるworkload — これらの場合はRoleを使用すべきです。
  • 複数の人でuserを共有する場合(CloudTrailで監査する能力が失われます)。

CLIを使用してuserを作成:

aws iam create-user --user-name order-service-deployer

aws iam create-login-profile \
  --user-name order-service-deployer \
  --password 'TempPass123!' \
  --password-reset-required

最初の行でuserを作成します。create-login-profileの行はConsoleログイン権を付与し、一時的なパスワードを設定して、初回ログイン時にパスワード変更を強制します — デフォルトパスワードが長く存在することを避けるためです。

よくある間違い:userのaccess keyを作成してコードにハードコードし、gitにコミットしてしまう。access keyを使用する必要がある場合は、定期的なrotationを有効にし、Secrets Managerを使用して保管してください。

4. Role:正しい場所での一時的な識別子

4.1. EC2 instance role

EC2 instanceがS3を呼び出す必要がある場合、access keyを.envに埋め込まないでください。roleを作成してinstance profileをアタッチしてください:

aws iam create-role \
  --role-name OrderServiceEc2Role \
  --assume-role-policy-document file://trust-policy.json

ファイルtrust-policy.jsonは、このroleをassumeすることが許可されているものを定義します:

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

Principal.Serviceはinstance profileを通じてこのroleをassumeすることが許可されるEC2サービスを指定します。これはtrust policyと呼ばれ、後で説明するpermission policyとは異なります。

4.2. Cross-account role

Roleはアカウント間でも使用され、他のAWSアカウントが認証情報を共有することなくあなたのアカウントにアクセスすることを許可します:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": { "AWS": "arn:aws:iam::111122223333:root" },
      "Action": "sts:AssumeRole",
      "Condition": {
        "StringEquals": { "sts:ExternalId": "order-partner-2024" }
      }
    }
  ]
}

sts:ExternalIdを持つConditionはconfused deputy attackに対抗します — 第三者がroleをassumeするには正しいExternalIdシークレットを知っている必要があり、別の第三者が公開されているARNを悪用するのを防ぎます。

5. Policy:構造と書き方

JSON policyはVersionとStatementで構成されます — 各statementにはEffect、Action、Resource、およびオプションのConditionがあります。

例:PaymentServiceが特定のS3 bucketに対して読み取り/書き込みを行うことを許可し、削除は許可しない場合:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowReadWritePaymentBucket",
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject"
      ],
      "Resource": "arn:aws:s3:::payment-receipts-bucket/*"
    },
    {
      "Sid": "DenyDeleteObject",
      "Effect": "Deny",
      "Action": "s3:DeleteObject",
      "Resource": "arn:aws:s3:::payment-receipts-bucket/*"
    }
  ]
}

最初のstatementはpayment-receipts-bucket/*パス上でのみGetObjectとPutObjectの2つのactionをAllowします。2番目のstatementは明示的に削除操作をDenyします — 後で誰かがs3:*policyを追加したとしても、明示的なdenはは常に優先されます。

このpolicyをroleにアタッチします:

aws iam put-role-policy \
  --role-name PaymentServiceRole \
  --policy-name PaymentBucketAccess \
  --policy-document file://payment-bucket-policy.json

5.1. Identity-based policy と resource-based policy

  • Identity-based policy:user/role/groupにアタッチされ、「このidentityは何ができるのか」に答えます。
  • Resource-based policy:リソース(S3 bucket policy、SQS queue policy、KMS key policy)に直接アタッチされ、「誰がこのリソースにアクセスできるのか」に答えます。

例:bucket policy(resource-based)が特定のroleのアクセスのみを許可する場合:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::123456789012:role/PaymentServiceRole"
      },
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::payment-receipts-bucket/*"
    }
  ]
}

これら2つのpolicyタイプは組み合わせることができます — 最終的な権限はその交差部分です(identity-basedとresource-basedの両方がallowである必要があります。どちらか1つがdenyの場合はdenyされます。ただし、cross-accountの場合は両方がallowである必要があります)。

6. Least Privilege原則

"Action": "s3:*"をざっくり使用するのではなく、必要な正確なactionをリストアップしてください。さらに範囲を絞るためにConditionを使用してください。

例:特定のVPCからのリクエストの場合のみobjectの読み取りを許可する場合:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::order-exports-bucket/*",
      "Condition": {
        "StringEquals": {
          "aws:SourceVpc": "vpc-0abc1234"
        }
      }
    }
  ]
}

aws:SourceVpcはグローバルcondition keyで、指定されたVPCのVPC endpointからのリクエストの場合のみallowします — データが内部ネットワークを離れないようにしたい場合に役立ちます。

Least Privilegeを達成するための実践的なアプローチ:

  1. 空のpolicy(デフォルトdeny)から始めます。
  2. workloadを実行し、IAM Access AnalyzerまたはCloudTrail logで、どのactionが拒否されているかを確認します。
  3. 正確なactionをpolicyに追加し、余分なものは追加しません。
  4. workloadが安定して実行され、ブロックされなくなるまで繰り返します。

注意:aws iam generate-service-last-accessed-detailsを使用して、user/roleが過去90日間で実際に使用したactionを確認してください — これにより、現在のpolicyから余分なactionを削除できます。

7. Policyの評価ロジックに関する重要な注意

AWSは以下の順序で権限を評価します。デバッグを迅速に行うために、これを覚えておく必要があります:

  1. デフォルトではすべてがdenyです(implicit deny)。
  2. 明示的なDenystatementがある場合 → 最終結果はdenyで、救う方法はありません。
  3. 明示的なdenがない場合、一致するAllowstatementを探します → allow。
  4. 一致するallowがない場合 → 依然としてdeny(デフォルトのimplicit deny)。
Request → Deny tường minh khớp? 
  ├─ Có → DENY (kết thúc)
  └─ Không → Allow khớp?
       ├─ Có → ALLOW
       └─ Không → DENY (implicit)

この順序を正しく理解することで、古典的なバグを避けることができます。例えば、AdministratorAccesspolicyをuserにアタッチしても、Organization レベルのSCP(Service Control Policy)がそのactionを明示的にdenyしている場合、actionがブロックされてしまいます。

8. Java AWS SDKを使用したroleのassumeのテスト

SDK v2の依存関係を追加:

<dependency>
    <groupId>software.amazon.awssdk</groupId>
    <artifactId>sts</artifactId>
    <version>2.28.11</version>
</dependency>

サービスがSTSを呼び出してroleをassumeし、一時的な認証情報を取得します:

public class RoleAssumer {

    private final StsClient stsClient;

    public RoleAssumer(StsClient stsClient) {
        this.stsClient = stsClient;
    }

    public Credentials assumePaymentServiceRole() {
        AssumeRoleRequest request = AssumeRoleRequest.builder()
                .roleArn("arn:aws:iam::123456789012:role/PaymentServiceRole")
                .roleSessionName("payment-service-session")
                .durationSeconds(900)
                .build();

        AssumeRoleResponse response = stsClient.assumeRole(request);
        return response.credentials();
    }
}

roleArnはassumeするroleを指定し、roleSessionNameはCloudTrailで追跡しやすいようにセッション名を設定し、durationSecondsは一時的な認証情報の有効期限を制限します — 短いほど安全です。

JUnit 5とMockitoを使用してテストを行い、正しいrole ARNを呼び出すロジックを検証します。実際のAWSを呼び出す必要はありません:

@Test
void assumePaymentServiceRole_shouldRequestCorrectRoleArn() {
    StsClient stsClient = mock(StsClient.class);
    Credentials fakeCredentials = Credentials.builder()
            .accessKeyId("AKIAFAKE")
            .secretAccessKey("secretFake")
            .sessionToken("tokenFake")
            .build();

    when(stsClient.assumeRole(any(AssumeRoleRequest.class)))
            .thenReturn(AssumeRoleResponse.builder()
                    .credentials(fakeCredentials)
                    .build());

    RoleAssumer assumer = new RoleAssumer(stsClient);
    Credentials result = assumer.assumePaymentServiceRole();

    ArgumentCaptor<AssumeRoleRequest> captor = ArgumentCaptor.forClass(AssumeRoleRequest.class);
    verify(stsClient).assumeRole(captor.capture());

    assertThat(captor.getValue().roleArn())
            .isEqualTo("arn:aws:iam::123456789012:role/PaymentServiceRole");
    assertThat(result.accessKeyId()).isEqualTo("AKIAFAKE");
}

このテストはStsClientをmockし、実際に送信されるrequestをキャプチャし、roleArnが期待通りであることをassertします。このアプローチにより、誰かが誤ってコード内のrole ARNをハードコードで間違って変更した場合、早期に検出することができます。

assumePaymentServiceRole_shouldRequestCorrectRoleArn() PASSED

9. 避けるべき一般的な間違い

  • すべてのservice accountにAdministratorAccessをアタッチする — 「後で最適化するため」という理由で。実際には誰も戻ってきて最適化することはなく、リスクは永遠に存在します。
  • EC2/Lambdaでのworkloadにaccess keyを使用する — roleの代わりに。access keyは漏洩しやすく、自動的にrotateされず、有効期限がありません。
  • roleのtrust policyをレビューするのを忘れる:Principalが広すぎる場合(例:"AWS": "*")、誰でもassumeできてしまいます。
  • 機密actionに対してMFAを使用したConditionを使用しない — (bucketの削除、IAM policyの編集など)。高リスクのactionについて"Bool": {"aws:MultiFactorAuthPresent": "true"}を追加してください。
  • 複数の責務を1つの大きなpolicyに混ぜる — レビューが難しくなります。resource/serviceごとに分離し、明確に命名してください(PaymentBucketReadWrite、MyPolicy1ではなく)。

IAMが複雑でない場合:個人的なサンドボックスで作業していたり、新しいサービスを学習していたりする場合、詳細なrole/policyを最初から作成すると学習速度が遅くなる可能性があります。その場合は、既存のmanaged policyを使用したtemprary userを使用してください(AmazonS3ReadOnlyAccessなど)。完了後に削除することを忘れないでください。

10. 結論

Userは手動でログインする必要がある人物向けで、Roleはworkloadと一時的なアクセス用で、Policyはallow/denyを決定するJSON形式のルール集です。評価の順序(明示的なdenが常に優先され、implicit denyがデフォルト)を正確に理解することで、権限の「奇妙な」問題が発生したときにすばやくデバッグできます。空のpolicyから始め、実際のlogに基づいて段階的に追加することでleast privilegeを適用し、コード内のrole assumeロジックに対して常にテストを書くことで、AWSシステムを安全に保つことができます。その後のメンテナンスに手間がかかりません。

awsiamsecuritycloudjava