4. IAM 用户与角色的直接策略附加审计
为什么“直接附加”是审计重点
在 IAM 中,用户或角色获得权限通常有三条路径:通过组继承、通过权限边界约束、以及直接附加策略。直接附加策略(内联策略或托管策略)会绕过组这一层集中管理,一旦人员转岗或角色复用,很容易留下无人认领的授权。本节任务是把所有“直接附加”的授权点找出来,并区分其风险等级。
审计对象与判定口径
- 直接附加:策略直接绑定在用户或角色实体上,而非绑定在组上。
- 内联策略:随实体生命周期存在,删除实体即消失,但难以跨实体复用和统一审计。
- 托管策略:独立存在的策略对象,可被多个实体引用,需关注是否为 AWS 托管(如 AdministratorAccess)还是客户自建。
- 风险信号:直接附加 + 高危动作(
*:*、iam:*、sts:AssumeRole)+ 无权限边界。
判定“直接附加”时,务必区分
AttachedPolicies(托管策略)与 InlinePolicies(内联策略),两者在 API 返回结构中字段不同,漏掉任一类都会造成审计盲区。审计数据流与判定路径
下图展示从 IAM 实体枚举到风险标记的完整数据流:先分别拉取用户和角色的直接附加策略,再与高危动作清单比对,最后按“是否设置权限边界”分流判定。
审计脚本实现
以下脚本使用 boto3 遍历所有用户和角色,分别提取托管策略和内联策略,并与高危动作清单比对。脚本输出 CSV 格式,便于后续筛选。
import boto3
import csv
import json
iam = boto3.client('iam')
# 高危动作前缀,可按需扩展
HIGH_RISK_ACTIONS = ['*', 'iam:*', 'sts:*', 'organizations:*']
def is_high_risk(policy_doc):
"""检查策略文档中是否包含高危 Allow 动作"""
for stmt in policy_doc.get('Statement', []):
if stmt.get('Effect') != 'Allow':
continue
actions = stmt.get('Action', [])
if isinstance(actions, str):
actions = [actions]
for act in actions:
for risk in HIGH_RISK_ACTIONS:
if act == risk or act.startswith(risk.replace('*', '')):
return True
return False
def get_inline_policy_doc(entity_type, name, policy_name):
if entity_type == 'user':
resp = iam.get_user_policy(UserName=name, PolicyName=policy_name)
else:
resp = iam.get_role_policy(RoleName=name, PolicyName=policy_name)
return resp['PolicyDocument']
def get_managed_policy_doc(arn):
# 获取默认版本
policy = iam.get_policy(PolicyArn=arn)['Policy']
version_id = policy['DefaultVersionId']
doc = iam.get_policy_version(PolicyArn=arn, VersionId=version_id)
return doc['PolicyVersion']['Document']
rows = []
# 遍历用户
for user in iam.list_users()['Users']:
uname = user['UserName']
# 托管策略
for ap in iam.list_attached_user_policies(UserName=uname)['AttachedPolicies']:
doc = get_managed_policy_doc(ap['PolicyArn'])
rows.append({
'entity_type': 'user', 'entity_name': uname,
'policy_type': 'managed', 'policy_name': ap['PolicyName'],
'policy_arn': ap['PolicyArn'],
'high_risk': is_high_risk(doc)
})
# 内联策略
for ip in iam.list_user_policies(UserName=uname)['PolicyNames']:
doc = get_inline_policy_doc('user', uname, ip)
rows.append({
'entity_type': 'user', 'entity_name': uname,
'policy_type': 'inline', 'policy_name': ip,
'policy_arn': '', 'high_risk': is_high_risk(doc)
})
# 遍历角色
for role in iam.list_roles()['Roles']:
rname = role['RoleName']
for ap in iam.list_attached_role_policies(RoleName=rname)['AttachedPolicies']:
doc = get_managed_policy_doc(ap['PolicyArn'])
rows.append({
'entity_type': 'role', 'entity_name': rname,
'policy_type': 'managed', 'policy_name': ap['PolicyName'],
'policy_arn': ap['PolicyArn'],
'high_risk': is_high_risk(doc)
})
for ip in iam.list_role_policies(RoleName=rname)['PolicyNames']:
doc = get_inline_policy_doc('role', rname, ip)
rows.append({
'entity_type': 'role', 'entity_name': rname,
'policy_type': 'inline', 'policy_name': ip,
'policy_arn': '', 'high_risk': is_high_risk(doc)
})
# 输出 CSV
with open('direct_policy_audit.csv', 'w', newline='') as f:
writer = csv.DictWriter(f, fieldnames=['entity_type','entity_name','policy_type','policy_name','policy_arn','high_risk'])
writer.writeheader()
writer.writerows(rows)
print(f"共发现 {len(rows)} 条直接附加策略,其中高危 {sum(1 for r in rows if r['high_risk'])} 条")
结果判读与处置建议
| entity_type | policy_type | high_risk | 建议动作 |
|---|---|---|---|
| user | managed | true | 立即移除直接附加,改由组继承;若必须保留,设置权限边界 |
| user | inline | true | 优先删除内联策略,迁移到托管策略并纳入组管理 |
| role | managed | true | 检查该角色是否被跨账号信任,评估是否需收窄信任策略 |
| role | inline | false | 记录并纳入定期复查,确认无权限膨胀 |
| user | managed | false | 低优先级,但建议统一迁移到组以简化管理 |
运行脚本前确保凭证具有
iam:List*、iam:Get* 权限。若账号下实体数量超过 100,需处理分页(list_users 和 list_roles 均支持 Marker 参数)。快速复查清单
- 是否同时覆盖了用户和角色两类实体?
- 是否同时提取了托管策略和内联策略?
- 高危动作清单是否包含
*、iam:*、sts:*? - 是否检查了权限边界(
PermissionsBoundary)字段? - 输出 CSV 是否包含实体名、策略名、策略类型和高危标记?