对应架构文档第 14 节。所有模块据此实现。这是冻结契约,改动需同步各模块。
- 生产:企业 OIDC/SSO 签发 JWT(RS256),控制面用 JWKS 验签。
- 开发:控制面内置本地签发端点用 HS256(密钥
AIOPS_AUTH_HS256_SECRET),便于无 IdP 也能端到端跑。 - 统一走
Authorization: Bearer <jwt>。
JWT claims(最小集):
AIOPS_AUTH_MODE=hs256 # hs256(开发内置签发) | oidc(生产) | disabled(仅限本地测试)
AIOPS_AUTH_HS256_SECRET=<dev-secret>
AIOPS_OIDC_ISSUER= # oidc 模式:IdP issuer
AIOPS_OIDC_JWKS_URL= # oidc 模式:JWKS
AIOPS_OIDC_AUDIENCE=aiops
POST /v1/auth/login { "username":"alice", "password":"..." } → { "token": "<jwt>", "expires_in": 3600 }
GET /v1/auth/me (Bearer) → { user claims }
开发用户库:内置若干演示账号(见实现),生产此端点关闭,由 IdP 负责。
角色 → 动作权限(RBAC):
| 角色 | 权限 |
|---|---|
viewer |
读 incidents/investigations/evidence |
oncall |
viewer + 启动/取消调查 + 反馈/确认/关闭 |
sre |
oncall + 全部命名空间 |
admin |
全部 + 未来管理动作 |
范围(ABAC):对涉及具体 Incident 的操作,校验
用户可访问范围(clusters/namespaces) ∩ Agent 服务权限 ∩ Incident 调查范围(cluster/namespace)
三者交集为空 → 403。实现见 internal/auth。
| 端点 | 要求 |
|---|---|
POST /v1/signals |
独立 webhook 鉴权(HMAC,见 §4),非用户 JWT |
GET /v1/incidents* |
viewer,且 ABAC 过滤到可见集群/命名空间 |
GET /v1/investigations*,/v1/evidence* |
viewer + ABAC |
POST /v1/incidents/{id}/investigations |
oncall + ABAC(Incident 范围) |
POST /v1/investigations/{id}/{cancel,feedback} |
oncall + ABAC |
/v1/auth/login |
公开(仅 hs256) |
/healthz,/metrics |
公开(metrics 生产应内网/加保护) |
内部 API :8090/internal/* |
仅集群内;AIOPS_INTERNAL_TOKEN 共享密钥头 X-Internal-Token |
- cluster-agent 作为 TLS 服务端,校验客户端证书(control-plane 持客户端证书)。
- 通过 env 开关,开发可关闭(明文),生产必开。
# cluster-agent
AIOPS_AGENT_TLS_ENABLED=true
AIOPS_AGENT_TLS_CERT=/certs/agent.crt
AIOPS_AGENT_TLS_KEY=/certs/agent.key
AIOPS_AGENT_TLS_CLIENT_CA=/certs/ca.crt # 校验调用方
# control-plane(作为客户端)
AIOPS_AGENT_MTLS_ENABLED=true
AIOPS_AGENT_CLIENT_CERT=/certs/client.crt
AIOPS_AGENT_CLIENT_KEY=/certs/client.key
AIOPS_AGENT_CA=/certs/ca.crt
AIOPS_CLUSTER_AGENT_URL=https://cluster-agent:9100
deploy/certs/ 提供本地自签脚本 gen-certs.sh(仅开发)。
POST /v1/signals 校验 HMAC-SHA256:
X-AIOPS-Signature: sha256=<hex(hmac(secret, raw_body))>
AIOPS_WEBHOOK_SECRET=<secret>
未配置 secret 时开发放行并记审计告警;配置后强制校验,失败 401。
POST /v1/incidents/{id}/investigations的Idempotency-Key落idempotency_keys表: 首次记录 (key, 结果 investigation_id);重复请求直接返回首个结果,不重复启动。
Tool Gateway 将脱敏后的 raw 证据 PutObject 到 MinIO(s3://aiops-evidence/<investigation_id>/<evidence_id>.json),
evidence.raw_ref 记录 object key。摘要入库,原文留对象存储(最小化进模型内容)。
Signal/Incident 消费重试超过 AIOPS_MAX_DELIVERY_ATTEMPTS(默认 5)进入 DLQ 表 dead_letters,并审计告警。
所有写操作与拒绝事件记 audit_log,actor 为 JWT sub(非 operator 缺省);权限拒绝记 result=denied + 原因。
所有密钥都只在启动时从环境变量读取,没有热重载。这是 K8s 下的常规形态
(改 Secret → 滚动重启),但 AIOPS_WEBHOOK_SECRET 有个额外的坑:
滚动重启期间一半副本持旧密钥、一半持新密钥。 Alertmanager 无论用哪个签名, 都会被另一半副本以 401 拒绝。而 Signal Ingress 的 401 意味着告警丢失 —— Alertmanager 重试几次就放弃,那段时间的故障在本系统里完全不存在, 而两边的日志都只显示"签名校验失败",看不出是轮换造成的。
因此 AIOPS_WEBHOOK_SECRET 支持逗号分隔的多个密钥,任一匹配即通过。
轮换按三步走:
# 1) 加新密钥(新在前,旧在后),滚动重启 —— 此时两种签名都收
AIOPS_WEBHOOK_SECRET="new-secret,old-secret"
# 2) 把 Alertmanager / cluster-agent 切到 new-secret
# 3) 摘掉旧密钥,滚动重启 —— 旧的失效
AIOPS_WEBHOOK_SECRET="new-secret"第 3 步不能省:只做前两步的话"轮换"只是多了一个密钥,泄漏的那个仍然可用。
几个实现细节:
- 比较对每个候选密钥都做完(不因某个匹配就提前 break),使比较次数与 "第几个密钥匹配"无关。
- 全空白(
" , ")会被解析成 0 个密钥,而 0 个密钥的行为是放行且不校验。 生产启动校验按"未设"拒绝这种值 —— 否则配置看起来设了、启动不报错, 而 Signal Ingress 实际上完全没有鉴权。 - 其余密钥(
AIOPS_AUTH_HS256_SECRET、AIOPS_INTERNAL_TOKEN)没有多值支持。 前者在 OIDC 生产模式下不使用(JWKS 轮换由 IdP 侧的 kid 机制处理, 本系统会按 kid 自动拉取新公钥);后者只在控制面内部调用间使用, 两端同时滚动重启的窗口内会有少量 5xx,由 Temporal 的 Activity 重试兜住。
Vault / KMS 与自动轮换属于部署侧:把 Secret 的来源换成 External Secrets Operator 或 CSI driver 即可,应用侧不需要改 —— 它只读环境变量。 上面那个多密钥机制正是为了让这类自动轮换不丢信号。
以 Keycloak 26 为例。这一节是实际跑通一遍后记下来的 —— 开箱配置会让 每个请求都 403,而症状指向错误的方向(见下)。
AIOPS_AUTH_MODE=oidc
AIOPS_OIDC_ISSUER=https://idp.corp.example/realms/aiops
AIOPS_OIDC_JWKS_URL=https://idp.corp.example/realms/aiops/protocol/openid-connect/certs
AIOPS_AUTH_AUDIENCE=aiopsIdP 侧需要三个 protocol mapper 与一处 realm 设置:
| 要什么 | 怎么配 | 不配的后果 |
|---|---|---|
aud 含 aiops |
audience mapper,included.custom.audience=aiops |
所有 token 因 audience 不匹配被拒(401) |
clusters claim |
user-attribute mapper,multivalued=true,claim.name=clusters |
ABAC 范围为空 → 每个请求 403 |
namespaces claim |
同上 | 同上 |
| 用户属性能写进 token | realm → users/profile → unmanagedAttributePolicy=ENABLED |
Keycloak 26 默认禁用非托管属性,上面两个 mapper 会静默输出空值 |
角色不需要 mapper:本系统会在顶层 roles 缺失时回落读
realm_access.roles(Keycloak 放 realm 角色的默认位置)。
clusters/namespaces 缺失与角色缺失的症状完全一样,而且极具误导性:
- token 校验通过 —— 日志是"认证成功"
- 审计记的是"合法身份被拒"(
result=denied) - 指标只是 403 上升
看到这三样,运维会去查 RBAC 角色配置或 ABAC 范围表 —— 而问题在 IdP 侧的 mapper。为此控制面会在"认证通过但零角色"时单独打一条 WARN 并指名这个原因 (按 subject 节流 5 分钟)。
另外两处形态差异已在代码里吸收,不需要额外配置:
aud是数组(["aiops","account"]),不是字符串sub是 UUID;本系统优先用preferred_username作为审计身份 —— 审计里记 UUID 等于记不了责任人
{ "sub": "alice", // 用户名 "email": "alice@corp.example", "roles": ["sre", "oncall"], // 角色 → 权限 "clusters": ["prod-cn-1"], // ABAC:可访问集群 "namespaces": ["payment","*"], // ABAC:可访问命名空间(* 全部) "exp": 1900000000, "iat": ..., "iss": "aiops-dev", "aud": "aiops" }