别再用主账号 AK 了:用 RAM 子账号把权限拆开
一个真实的教训
去年我在管理华为云资源时,图省事直接用了账号的 AK/SK 写进本地脚本。后来这个 AK 因为一次误操作被写进了某个会同步到第三方的地方,等我发现去控制台处理时,那把 AK 已经躺在我根本不知道的地方了。
处理结果还算体面——控制台里作废旧 AK、重新签发。但这次之后我做了一件事:把所有脚本里用的 AK 全换成了 RAM 子账号的 AK,并按用途拆成了几把,每把只给必要的权限。
这篇写的是这套做法怎么落地,以及华为云 RAM 里几个容易搞混的概念。
一、为什么要拆
先说清楚风险到底在哪。
主账号(或者说账号本身)的 AK 是全能钥匙:它能操作你名下所有资源,包括删除服务器、修改账单、创建新的子账号、给别的账号授权。它一旦泄露,攻击者能做的事情远超"跑跑脚本"的范围。
而 RAM 子账号的 AK 只能做你显式授权的事。哪怕这把钥匙被完整拿走,攻击者能造成的破坏也被框在你能接受的范围内。
实际收益有这么几层:
- 爆炸半径可控。跑测试脚本用的 AK,只给测试资源的权限;跑部署脚本的 AK,给发布相关的权限。就算前者泄露,也动不了生产数据库。
- 能单独作废。某台机器上的脚本需要撤权,直接删掉那把 AK 的 access key id 就行,不用动账号本身。
- 留痕可审计。子账号的操作在 CES/CTS 里能按身份区分开,出问题时知道是谁干的、哪个脚本干的。
- 能轮换。一把子账号 AK 用在若干台机器上,轮换时你会真的需要一个"更新所有机器"的流程;而主账号 AK 的轮换是件让人望而生畏的苦差。
二、IAM 模型的四个概念
华为云的 RAM(Resource Access Management)里,这四个概念必须分清楚:
| 概念 | 是什么 | 类比 |
| 用户(User) | 一个独立的身份,有自己的登录凭证 | 一个员工 |
| 用户组(Group) | 一组用户的容器,方便批量授权 | 一个部门 |
| 策略(Policy) | 一段 JSON,描述"能对哪些资源做什么操作" | 岗位权限说明 |
| 委托(Agency) | 让其他账号(或服务)以你的身份操作 | 外部承包商的临时授权 |
委托和用户最容易混。区别在于:
- 用户是账号内部的身份,你自己管理它的生命周期。
- 委托是把权限临时借给别的账号(比如让对象存储服务代表你访问你的桶),或者让服务角色(Service role)代表你操作。
日常脚本场景用用户就够了。
三、创建用户
先装 SDK:
pip3 install huaweicloudsdkiam -i https://pypi.org/simple
huaweicloudsdkiam 有多个版本目录,用户管理相关接口在 v5:
from huaweicloudsdkcore.auth.credentials import BasicCredentials
from huaweicloudsdkiam.v5 import IamClient
from huaweicloudsdkiam.v5.region.iam_region import IamRegion
credentials = BasicCredentials(ak=AK, sk=SK, project_id=PROJECT_ID)
client = IamClient.new_builder() \
.with_credentials(credentials) \
.with_region(IamRegion.value_of("cn-north-4")) \
.build()
注意 IAM 是全局服务,控制台在华北-北京四,但接口调用时用哪个区域都行(SDK 会自动路由)。
创建用户:
from huaweicloudsdkiam.v5 import CreateUserV5Request
from huaweicloudsdkiam.v5.model import CreateUserReqBody
body = CreateUserReqBody(
name="deploy-bot",
description="部署脚本专用子账号",
enabled=True
)
request = CreateUserV5Request(body=body)
response = client.create_user_v5(request)
user_id = response.user.id
几个字段的坑:
- body 模型名不叫
CreateUserV5RequestBody,而是CreateUserReqBody。IAM v5 的 SDK 里 Request 后缀和 ReqBody 后缀混用,import 前最好先确认一下:
```python
import huaweicloudsdkiam.v5.model as m
print([n for n in dir(m) if 'CreateUser' in n])
```
enabled=True才有意义。默认创建出来可能是禁用的,禁用的用户连 AK 都签发不了。- 返回值里是
response.user.id,这个 id 后面签发 AK、加组、授权都要用,别用name当标识。
四、创建用户组并加用户
一个用户一个角色会很乱。用组来批量管理:
from huaweicloudsdkiam.v5 import CreateGroupV5Request, AddUserToGroupV5Request
from huaweicloudsdkiam.v5.model import CreateGroupReqBody, AddUserToGroupReqBody
group_body = CreateGroupReqBody(
group_name="deployers",
description="部署相关子账号"
)
group = client.create_group_v5(CreateGroupV5Request(body=group_body))
group_id = group.group.id
client.add_user_to_group_v5(
AddUserToGroupV5Request(
group_id=group_id,
body=AddUserToGroupReqBody(user_id=user_id)
)
)
同样是 CreateGroupReqBody 而不是 CreateGroupV5RequestBody。
五、写策略
这是最需要精确的部分。策略是一段 JSON,结构固定:
{
"Version": "1.1",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ecs:server:list",
"ecs:server:show"
],
"Resource": [
"*"
]
}
]
}
各字段的含义:
Version:写"1.1"。Statement:数组,一个策略可以有多条。Effect:"Allow"或"Deny"。华为云支持显式 Deny,且 Deny 优先级高于 Allow——用户同时匹配到一条 Allow 和一条 Deny 时,结果是拒绝。这点和 AWS 一致。Action:操作标识,格式是服务:资源:动作,比如ecs:server:list、vpc:securityGroups:update。也可以用通配符ecs:*:*。Resource:作用范围。*表示全部;如果要限定,就写资源 ARN 或资源 ID。
Resource 到底该写多细,取决于你想要什么粒度。 三种常见写法:
// 最宽松:所有实例的所有操作 "Resource": ["*"], "Action": ["ecs:*:*"] // 中等:所有实例,但只读 "Resource": ["*"], "Action": ["ecs:server:list", "ecs:server:show"] // 最严格:只对指定实例 "Resource": ["*"], "Action": ["ecs:server:start", "ecs:server:stop"]
对 ECS 这类实例级资源,SDK 层面把 Resource 写成 "*" + 精确 Action 是常见做法。真正需要限定到某个实例时,Resource 要填该实例的 ARN,形如:
arn:cn-east-3:iam:xxxxxxxx:instance/i-yyyyyyyyyyyyy
写策略:
from huaweicloudsdkiam.v5 import CreatePolicyV5Request
from huaweicloudsdkiam.v5.model import CreatePolicyReqBody, PolicyDocument
policy_doc = {
"Version": "1.1",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ecs:server:list",
"ecs:server:show"
],
"Resource": ["*"]
}
]
}
body = CreatePolicyReqBody(
policy_name="ecs-readonly",
path="/custom/ecs-readonly/",
policy_document=PolicyDocument(**policy_doc),
description="只读查看 ECS 实例"
)
policy = client.create_policy_v5(CreatePolicyV5Request(body=body))
policy_id = policy.policy.id
关于 PolicyDocument 这个类型:它的 openapi_types 是空的,意思就是"接受任意 JSON"。直接传 dict 就行,不用包 PolicyDocument(...):
policy_document=policy_doc
包一层也不报错,但没必要。
path 参数决定策略在控制台里的归类,/custom/ 开头表示自定义策略。这个字段不填也能创建,但控制台里会归到默认位置。
六、把策略绑给用户或组
from huaweicloudsdkiam.v5 import AttachUserPolicyV5Request, AttachGroupPolicyV5Request
from huaweicloudsdkiam.v5.model import AttachUserPolicyReqBody
# 绑给用户
client.attach_user_policy_v5(
AttachUserPolicyV5Request(
policy_id=policy_id,
body=AttachUserPolicyReqBody(user_id=user_id)
)
)
# 或者绑给组(推荐,批量维护更省事)
client.attach_group_policy_v5(
AttachGroupPolicyV5Request(
policy_id=policy_id,
body=AttachGroupPolicyReqBody(group_id=group_id)
)
)
绑给组而不是绑给用户,理由很直接:人员流动时只需要把人移出组,权限跟着组走,不用逐个用户重新授权。
七、签发 AK
一切就绪,给用户签发访问密钥:
from huaweicloudsdkiam.v5 import CreateAccessKeyV5Request
response = client.create_access_key_v5(
CreateAccessKeyV5Request(user_id=user_id)
)
print(response.access_key.access_key_id)
print(response.access_key.secret_access_key)
secret_access_key 只在这一次响应里返回,之后再也查不到了。 这是设计如此,不是 bug。所以:
# 拿到立刻落盘或交给部署系统,别指望能再查一次 ak = response.access_key.access_key_id sk = response.access_key.secret_access_key
如果丢了,只能重新签发一把新的,然后作废旧的。
一个用户默认最多两把 AK,签发第三把会报错。轮换时可以先签新的、验证能用、再删旧的。
八、查看和清理
from huaweicloudsdkiam.v5 import ListAccessKeysV5Request, ShowAccessKeyLastUsedV5Request
# 列出某用户的所有 AK
keys = client.list_access_keys_v5(ListAccessKeysV5Request(user_id=user_id))
for k in keys.access_keys:
print(k.access_key_id, k.status, k.created_at)
# 看某把 AK 最后一次使用时间
last = client.show_access_key_last_used_v5(
ShowAccessKeyLastUsedV5Request(user_id=user_id, access_key_id=ak_id)
)
print(last.last_used_at)
last_used_at 这个字段是排查 AK 泄露最实用的工具——如果一把你以为是闲置的 AK 最近有使用记录,就值得警觉。
删除:
from huaweicloudsdkiam.v5 import DeleteAccessKeyV5Request
client.delete_access_key_v5(
DeleteAccessKeyV5Request(user_id=user_id, access_key_id=ak_id)
)
九、几条实践经验
按用途拆 AK,别按人拆。 我的划分方式是按"用途 + 环境":部署用一套、巡检用一套、数据同步用一套。同一个用途的多台机器共用一把,轮换时有明确的目标。
先只读,后读写。 新建的子账号先只给 list/show 类权限,跑通了再加写操作。绝大多数运维脚本其实只需要读权限。
把 Deny 当护栏用。 除了 Allow,可以再给一条显式 Deny 保护高危操作:
{
"Effect": "Deny",
"Action": ["ecs:server:delete", "iam:*:delete"],
"Resource": ["*"]
}
这条策略和上面的 Allow 绑给同一个用户,Deny 优先级更高,即便 Allow 里写了删除也会被拦住。
别把 AK 写进代码仓库。 放在环境变量或者 CI 的加密变量里。本地的话可以用 ~/.config/ 下权限 600 的文件,加载时立刻检查文件权限:
stat -f '%Sp %N' ~/.config/huaweicloud/ak
macOS 上看到 -rw------- 才是对的。
定期用 API 巡检。 列出所有子账号的 AK 和最后使用时间,把"三个月没用过"的清理掉。这件事手动做一次很烦,写成脚本跑一次就很轻松。
小结
用 RAM 子账号替代主账号 AK,本质是把"一把万能钥匙"换成"一把把用途明确的钥匙"。改造成本不高——多建几个用户和组、把手里的 AK 换掉——但收益是实打实的:泄露了损失可控、能单独撤权、能审计、能轻松轮换。
具体落地顺序我建议这样:
- 梳理现有脚本用到的 AK 和它们实际需要什么操作
- 建对应的用户组,每组配一份精确到 Action 的策略
- 建用户、加组、签发 AK
- 逐个把机器上的旧 AK 换成新的
- 作废主账号 AK——这一步做完,前面所有的风险才真正降下来
- 挂一个定期巡检脚本,列出所有 AK 的最后使用时间
第 5 步做完的那一刻,你会意识到之前一直在裸奔。
- 点赞
- 收藏
- 关注作者
评论(0)