IAM: 正确使用 User、Role 和 Policy

1. 概览

IAM (Identity and Access Management) 是 AWS 的基础服务,决定了“谁能做什么,对什么资源”。很多团队从一开始就用错了:给所有人创建有管理员权限的用户,为了快速就附加 *:* 的策略,或者混淆了 user 和 role 的概念。本文帮你理清 user、role、policy 这三个核心概念,正确编写 policy JSON,应用最小权限原则,并用 Java AWS SDK 测试 assume role。

读完之后,你将能自信地设计一套精简、安全的 IAM 体系,而不是给所有服务分配“上帝级权限”。

2. 三个核心组件

IAM 围绕三个概念展开:

  • User:代表某个具体的人或应用,具有长期有效的凭证(密码、访问密钥)。
  • Role:临时性的身份,没有固定凭证,由 user、service 或其他账户“assume”(扮演)。
  • Policy:JSON 文本,描述权限(allow/deny),可以附加到 user、role、group 或资源。

注意:Role 不是“没有密码的 user”。Role 生成临时凭证(有时间限制,通过 STS),而 user 使用固定凭证直到你轮换它们。

3. User:何时使用,何时不用

User 适合:

  • 人类需要登录 Console 进行手动操作。
  • 特殊情况下无法使用 role(少见,如 AWS 外的遗留系统需要固定的访问密钥)。

User 不适合:

  • 在 EC2、Lambda、ECS 上运行的工作负载 — 这些应该使用 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 创建访问密钥,然后硬编码到代码里,提交到 git。如果必须使用访问密钥,要启用定期轮换,并用 Secrets Manager 存储。

4. Role:正确位置的临时身份

4.1. EC2 instance role

当 EC2 实例需要访问 S3 时,不要在 .env 里嵌入访问密钥。要创建 role 并附加 instance profile:

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

文件 trust-policy.json 指定了谁可以 assume 这个 role:

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

Principal.Service 指定 EC2 service 通过 instance profile 被允许 assume 这个 role。这叫做 trust policy — 不同于后文描述的 permission policy。

4.2. 跨账户 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 防止混淆代理攻击 — 第三方必须知道正确的 ExternalId 密钥才能 assume role,避免某个第四方利用公开的 ARN。

5. Policy:结构和编写方法

一个 policy JSON 包含 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 Allow 两个 action GetObject 和 PutObject,只作用于 payment-receipts-bucket/* 路径。第二个 statement Deny 明确禁止删除操作 — 即使后来有人附加了包含 s3:* 的 policy,明确的 deny 仍会生效。

将此 policy 附加到 role:

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

5.1. 基于身份的 policy vs 基于资源的 policy

  • 基于身份的 policy:附加到 user/role/group,回答“这个身份能做什么”。
  • 基于资源的 policy:直接附加到资源(S3 bucket policy、SQS queue policy、KMS key policy),回答“谁能访问这个资源”。

基于资源的 policy 示例(bucket policy),只允许特定 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/*"
    }
  ]
}

这两种 policy 可以结合 — 最终权限是两者的交集(基于身份的和基于资源的都必须允许;如果任何一个拒绝则被拒绝;跨账户情况下两个都必须 allow)。

6. 最小权限原则

不要为了快速就写 "Action": "s3:*",应该列出确实需要的 action。用 Condition 进一步缩小范围。

例子:只有从特定 VPC 来的请求才能读对象:

{
  "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 是全局条件键,只有当请求来自指定 VPC 的 VPC endpoint 时才 allow — 当你不想让数据流出内部网络时很有用。

达成最小权限的实用方法:

  1. 从空 policy 开始(默认拒绝)。
  2. 运行工作负载,查看 IAM Access Analyzer 或 CloudTrail 日志,哪些 action 被拒绝。
  3. 将那个确切的 action 添加到 policy,不要添加多余的。
  4. 重复直到工作负载稳定运行且没有被拒绝。

提示:用 aws iam generate-service-last-accessed-details 查看某个 user/role 在过去 90 天内实际使用过哪些 action — 可以从现有 policy 中删除未使用的 action。

7. 需要记住的 policy 评估逻辑

AWS 按以下顺序评估权限,掌握这一点能帮你快速 debug:

  1. 默认所有请求被 deny(隐式拒绝)。
  2. 如果有任何 statement 明确写 Deny 且匹配 → 最终结果是 deny,无可挽回。
  3. 如果没有明确的 deny,找是否有匹配的 Allow statement → allow。
  4. 如果没有任何匹配的 allow → 仍然 deny(隐式拒绝为默认)。
请求到达 → 是否有匹配的明确 Deny?
  ├─ 是 → DENY(结束)
  └─ 否 → 是否有匹配的 Allow?
       ├─ 是 → ALLOW
       └─ 否 → DENY(隐式)

理解这个顺序能帮你避免经典 bug:给某个 user 附加 AdministratorAccess 却仍然被某个 action 拒绝,因为组织级别的 SCP(Service Control Policy)明确 deny 了那个 action。

8. 用 Java SDK 测试 assume role

添加 SDK v2 依赖:

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

服务调用 STS 来 assume role 并获取临时凭证:

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 为 session 命名以便在 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");
}

这个测试 mock 了 StsClient,capture 实际发送的请求,然后 assert roleArn 符合预期。这样能帮你尽早发现如果有人不小心修改了代码中硬编码的 role ARN。

assumePaymentServiceRole_shouldRequestCorrectRoleArn() PASSED

9. 需要避免的常见错误

  • 给所有 service account 分配 AdministratorAccess,理由是“留着以后再优化”。实际上没人会回头优化,风险会永久存在。
  • 在 EC2/Lambda 上的工作负载使用访问密钥而不是 role — 访问密钥容易泄漏,无法自动轮换,没有时间限制。
  • 忘记检查 role 的 trust policy:Principal 太宽泛(比如 "AWS": "*")导致任何人都能 assume 该 role。
  • 对敏感操作不使用 Condition 和 MFA(删除 bucket、修改 IAM policy)。为这些高风险 action 添加 "Bool": {"aws:MultiFactorAuthPresent": "true"} 条件。
  • 把太多责任混在一个大 policy 里,难以审查。应该按资源/服务拆分,取清晰的名字(PaymentBucketReadWrite 而不是 MyPolicy1)。

何时不需要复杂的 IAM:如果你在做个人沙箱、学习新服务,从一开始就设计复杂的 role/policy 可能会拖累学习速度。此时用临时 user 加上现成的 managed policy(如 AmazonS3ReadOnlyAccess)就够了,完成后记得删除。

10. 总结

User 用于需要手动登录的人,role 用于工作负载和临时访问,policy 是 JSON 规则集决定 allow/deny。掌握评估顺序(明确 deny 永远优先,隐式 deny 为默认)能帮你快速 debug 诡异的权限问题。通过从空白开始、基于实际日志逐步添加权限的方式应用最小权限原则,并为代码中的 assume role 逻辑编写测试 — 这是保持 AWS 系统安全同时减少后续维护成本的方式。

awsiamsecuritycloudjava