在 Qoni 这类托管平台上跑 Agent,密钥、环境配置、运行日志建议拆成三个权限层级来管:密钥层只保留「发布时注入」和「运维应急读取」两条通道,日常开发不应看到明文;环境配置层可以按项目读、按发布写;运行日志层按服务或环境划范围,导出权限单独控制。真正容易出事的不是层级数量,而是同一个账号既能把密钥打进日志,又能把日志导出去。分权之后还要用一次真实调用去日志里搜敏感串,确认脱敏规则真的生效,而不是只在配置里写了规则。
多人共用托管平台时,先把对象分清:密钥、环境配置、运行日志敏感度不同,不要塞进同一个权限组。可先按三层来分——密钥读写限发布与运维,环境配置开发可读、发布可写,运行日志按服务或环境隔离并单独控制导出。分权后要做一次验证:发一次调用,用命令在日志里搜 api_key、token、password 等关键词,结果为空才算脱敏生效;命中则先轮换密钥,再修打印逻辑。边界是:这套做法只缩小泄露面,不能替代轮换与留痕。
列出平台上的密钥、环境配置、运行日志三类对象
分权之前先做一次盘点,把平台上真实存在的对象按敏感度归类。可以照下面这张表对着平台成员列表逐项打勾,重点记录「实际持有权限」和「应该持有权限」的差集,差集就是你要处理的部分。
| 对象 | 读取方 | 写入方 | 敏感程度 | 常见现状 |
|---|---|---|---|---|
| 密钥(模型 API key、数据库口令、第三方 token) | 发布流程、运维应急 | 密钥管理员或发布流水线 | 高 | 常同时躺在开发本地配置和平台变量里,多数成员可读 |
| 环境配置(模型名、超时、并发上限、端点地址) | 开发、发布、运维 | 发布环节 | 低到中 | 常与密钥同一个权限组,开发能直接改线上取值 |
| 运行日志(调用记录、错误栈、请求头、耗时) | 开发读本环境,运维读全部 | 运行时自动写入,人一般不写 | 中到高,取决于是否打印请求体 | 多数成员可读、可导出,导出行为不额外审计 |
盘点时用一个低权限账号做拒绝性验证:让它尝试读取某个密钥、导出一次线上日志,如果都能成功,说明当前权限比你以为的宽。这一步只记录「通过或被拒」,不需要把密钥内容复制到工单里。
按角色划出最小权限集合
角色按开发、发布、运维三类划分,通常够用。核心原则是:读密钥明文的人和改日志保留策略的人不要是同一批账号。
| 角色 | 可读 | 可写 | 明确不给 |
|---|---|---|---|
| 开发 | 本环境环境配置、本环境近期日志 | 代码、本环境配置草稿 | 密钥明文、线上日志导出 |
| 发布 | 密钥的注入通道(只注入、不回显)、全环境配置 | 密钥新版本、配置变更、发布记录 | 查看历史密钥明文 |
| 运维 | 全部日志、密钥元数据(名称、版本、更新时间) | 权限配置、日志保留与导出策略 | 默认不可读密钥明文 |
越权动作要有固定路径:需要临时读取密钥明文或导出线上日志时,走「申请—审批—限时授权—事后审计」,授权带到期时间,到期自动收回。不要用临时把某人提升为管理员这种方式绕过,那样权限表会失真。授权生效后,用被授权的账号做一次允许性验证,确认拿到的是要用的那一项,而不是整个项目组。
把密钥改为仅发布时注入、运行时不回显
目标是把密钥的出现位置压缩到一处:发布流程。Agent 进程只从环境变量或只读挂载文件里取值,自身不打印、不写文件、不放进 URL 查询串。
# 仅发布阶段执行:从密钥管理器取值,注入本次部署的进程环境
export AGENT_API_KEY=$(YOUR_SECRET_CMD get agent/api-key `--env`=prod)
# 运行时只读取,不再回显
python -m agent.run `--config` /etc/agent/config.yamlYOUR_SECRET_CMD 换成平台自带的密钥读取命令或 CI 的变量注入方式;config.yaml 里只写引用名,例如 api_key_env: AGENT_API_KEY,不写明文。下面是脱敏打印的通用骨架,放在 Agent 入口处即可生效:
import os, re, logging
SECRET_NAME_HINTS = ('API_KEY', 'TOKEN', 'PASSWORD', 'SECRET')
SECRET_VALUE_RE = re.compile(r'(?i)(api[_-]?key|token|password|authorization)=([^\s&]+)')
def mask(value):
if not value or len(value) < 8:
return '***'
return value[:4] + '***' + value[-2:]
def safe_env():
return {
k: (mask(v) if any(h in k.upper() for h in SECRET_NAME_HINTS) else v)
for k, v in os.environ.items()
}
class RedactFilter(logging.Filter):
def filter(self, record):
record.msg = SECRET_VALUE_RE.sub(lambda m: m.group(1) + '=***', str(record.msg))
return True
logging.getLogger().addFilter(RedactFilter())
logging.info('agent env: %s', safe_env())需要避免的写法:把整个请求头对象丢进日志(logging.info('headers=%s', headers))、直接打印全部环境变量或整个配置对象、把密钥拼进请求 URL 的查询参数、在异常信息里回显用户传入的 token。这几类都会绕过 filter,因为 filter 只处理日志消息本身,不处理已经落盘的字符串。
发起一次调用后检查日志里的敏感串
脱敏规则写没写对,看日志才算数。先发一次会用到密钥的调用,再在对应时间窗内搜关键词:
# 单机日志文件
grep -rniE '(api[_-]?key|token|password|authorization|bearer)[=:]' ./logs/
# 托管平台或容器日志,取最近 10 分钟
kubectl logs deploy/agent `--since`=10m | grep -niE '(api[_-]?key|token|password|authorization|bearer)'
# 确认某一把具体密钥有没有落到日志里,只用前缀,不要用完整值
grep -rnF 'YOUR_KEY_PREFIX' ./logs/结果为空,说明这个时间窗和这些关键词下没有明文,记录命令、时间和节点即可。命中时的处理顺序建议是:先暂停把该日志共享或外发,再轮换被命中的密钥,然后定位打印位置加上脱敏或删掉打印语句,最后重发一次同样的调用、跑同一条命令确认。要留意 grep 只覆盖你指定的时间窗和关键词,历史归档日志需要另跑一次,日志保留期越长,检查范围越大。
记录权限变更流程
分权不是一次性动作,成员进出、环境新增都会让权限表漂移,所以每一步都要留痕,记录集中放一处,保证能追到「谁在什么时候因为什么拿到过什么」。
- 变更申请:写清对象(哪个密钥、哪个环境配置、哪个日志范围)、申请人角色、理由、需要的期限和影响面,登记工单号。
- 审批:密钥和线上日志导出建议至少两人确认,审批人不能是申请人本人;记录审批人、审批时间和批准的范围与期限。
- 生效验证:变更后做一次允许性或拒绝性测试,记录执行的命令、时间和输出(输出里不要带密钥明文)。
- 回收:离职或换岗当天收回角色,按需轮换其接触过的密钥;定期把成员列表和权限表对一次,记录差集和处理结果。
- 留痕归档:权限表、申请与审批记录、验证输出、回收记录放在同一处,保留到团队审计要求的时间。
这套流程跑顺之后,判断权限是否合理会变得简单:任何时刻看一个账号,都能说清它能读什么、能写什么、上一次变更是什么时候、谁批的。