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 — 当你不想让数据流出内部网络时很有用。
达成最小权限的实用方法:
- 从空 policy 开始(默认拒绝)。
- 运行工作负载,查看 IAM Access Analyzer 或 CloudTrail 日志,哪些 action 被拒绝。
- 将那个确切的 action 添加到 policy,不要添加多余的。
- 重复直到工作负载稳定运行且没有被拒绝。
提示:用
aws iam generate-service-last-accessed-details查看某个 user/role 在过去 90 天内实际使用过哪些 action — 可以从现有 policy 中删除未使用的 action。
7. 需要记住的 policy 评估逻辑
AWS 按以下顺序评估权限,掌握这一点能帮你快速 debug:
- 默认所有请求被 deny(隐式拒绝)。
- 如果有任何 statement 明确写
Deny且匹配 → 最终结果是 deny,无可挽回。 - 如果没有明确的 deny,找是否有匹配的
Allowstatement → allow。 - 如果没有任何匹配的 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 系统安全同时减少后续维护成本的方式。