我遇到的一次故障里,一个 producer 服务连续八个多小时没往 Kafka 写进一条消息。主链路处理正常,consumer lag 也没有异常,最后是客户先发现历史查询接口查不到新数据。触发点是同一时段有人在 Confluent Cloud 上新建了一把 API Key,以为配的 ACL 只作用在这把新 key 上;那把新 key 和服务在用的那把,挂在同一个 service account 下。

机制部分照 Confluent 官方文档核对过(2026-09-12 查的),命令写法照 CLI reference,下面的命令里 lkc-sa-demo- 都是占位 ID。

API Key 只是凭据,权限挂在 service account 上#

Confluent Cloud 的 API key 文档有一句话把它说死了:

Permissions are not associated with an API key, but with the user or service account.

同一页还写着 “Each API key is associated with a specific user account or service account”。API Key 承担的是认证,回答「我是谁」;授权归 ACL 和 role binding(RBAC 里把某个角色绑到某个账号上的那条记录)管,它们认的是账号。

一个 service account 名下可以挂很多把 key,Kafka 这种绑在具体 cluster 上的 key,默认配额是 100 把(quota code iam.max_cluster_api_keys.per_service_account,见 service quotas)。一次权限改动的作用面,就是账号名下的所有 key。

落到 ACL 上就是 principal 那一栏。CLI 查 ACL 可以按 principal 过滤,格式是 User: 前缀加账号 ID:

confluent kafka acl list --cluster lkc-123456 --principal User:sa-123456

建 ACL 时给的同样是 service account ID,整个参数表里没有 API Key 的位置:

confluent kafka acl create --allow --service-account sa-123456 \
  --operations write --topic demo-topic

所以「给新配的这把 key 单独一组 ACL」这个动作,在 Confluent 的模型里没有对应物。它改的是整个 service account 的权限,账号名下每一把 key 跟着一起变,影响面是所有共用这个账号的服务。权限被拒时协议返回 TOPIC_AUTHORIZATION_FAILED(Kafka 4.3 协议文档里的错误码 29),Java client 抛 TopicAuthorizationException。这次就是这样:报错只在应用日志里,集群指标上一切正常,八个多小时没人发现。要早点发现,得给这类 topic 的写入单独配一条监控。

账号往上一层还有 cluster#

service account 是组织级资源,ACL 却是按 cluster 存的。Service Accounts 那页的原话是 “Service accounts span the entire organization and can own API keys for many different resources, including development and production clusters.",官方 ACL 排障页给的另一半是 “service account ACLs are specific to Kafka clusters”。两句叠起来的结构是这样:

flowchart TD SA["service account sa-123456
组织级:一个账号横跨所有 cluster"] subgraph PROD["cluster lkc-prod"] KP["key 1、key 2"] AP["ACL 一套
principal User:sa-123456"] end subgraph STG["cluster lkc-staging"] KS["key 3"] AS["ACL 另一套
principal 还是 User:sa-123456"] end SA --> KP --> AP SA --> KS --> AS

一次 ACL 改动的作用面就是「账号 × cluster」这一格:改 prod 这套,staging 那套一个字不动;而在 prod 这一格里,改动横扫 key 1 和 key 2。confluent kafka acl 一次只作用在一个 --cluster 上,多 region、多 cluster 的部署里同一份权限要在每套 cluster 上各落一次,列影响范围也按格子分开列。

数一格里有几把 key 就是这条命令:

confluent api-key list --resource lkc-123456 --service-account sa-123456

这张表就是改动的作用范围,不过表里能对上用途,靠的是建 key 时写进去的 --descriptionapi-key createapi-key update 都有这个 flag),没有填写约定就只剩一串 key ID。这组命令里 cluster 参数叫 --resource,acl 那组叫 --cluster,传的同一个 lkc- ID;账号 ID 用 confluent iam service-account list 查。

DENY 先求值,压过 ALLOW 和 role binding#

作用面再大,加一条 ACL 通常也只是多给权限,写入不该因此停掉。这次停掉,是因为新配的那条对那个 topic 的 WRITE 给的是 DENY,而原来给那个服务的 ALLOW WRITE 条目一直都在。Confluent 讲 ACL 与 RBAC 混用的那一页写得很直接:

ACL DENY rules are applied first. If an ACL DENY is applied, then access is denied regardless of any ACL ALLOW rules and any RBAC role bindings.

同一段紧接着写 “All RBAC roles are ALLOW”,所以 DENY 只能从 ACL 这一侧配进来。一次 WRITE 要过的判断是这样:

flowchart TD P["producer 用服务在用的那把 key 发 WRITE"] --> R["解析成 principal:User:sa-123456"] K["这次新建的 API Key"] -.->|"认证后落到同一个 principal"| R R --> D{"账号上有 DENY 命中?"} D -->|"有:这次新配的 WRITE DENY"| X["TOPIC_AUTHORIZATION_FAILED
后面的 ALLOW 和 role binding 都不再看"] D -->|没有| A{"ACL ALLOW 或 role binding 命中?"} A -->|"有:原来那条 WRITE ALLOW"| OK["放行"] A -->|都没有| N["没有显式授予,不放行"]

DENY 是一条独立条目,不会去动已有的 ALLOW,被它盖住的那个 principal 底下不留痕迹。要收窄权限就走删 ALLOW,或者换成范围更小的 ALLOW;在 prod 上叠 DENY 之前,得先想清楚这个账号名下都有谁。

同一个 topic 上要两套权限,就要两个 service account#

那次操作的意图,是给同一个 topic 配一套和现有服务不同的权限。按上面三条,这件事在一个 service account 里做不成。

官方 best practice 也在 API key 那一页:

A best practice is to create separate service accounts associated with an API key for each application or use case to narrow the operational impact of retiring a specific API key.

文档给的理由是缩小轮换 key 时的影响面;改权限时的影响面也按账号算,是同一个道理。

一个只写、一个只读,就是两个账号各配各的。注意 consumer 那边除了 topic 上的 READ,还要一条 consumer group 上的 READ(官方 create 那页的例子在 group 上给的是 read,describe):

confluent iam service-account create demo-writer --description "producer"
confluent iam service-account create demo-reader --description "consumer"

confluent kafka acl create --allow --service-account sa-111111 \
  --operations write --topic demo-topic
confluent kafka acl create --allow --service-account sa-222222 \
  --operations read,describe --topic demo-topic
confluent kafka acl create --allow --service-account sa-222222 \
  --operations read,describe --consumer-group demo-group

教训#

这次的直接原因是知识缺口:我们这边当时没人知道 ACL 认的是 service account,以为多开一把 key 就多出一条权限边界。

生产环境严格按 runbook 执行。runbook 里没有、自己又没把握的操作,先在 staging 上跑一遍。

知识缺口补不完,下次还会有谁都没做过的生产操作。staging 就是为这种情况准备的:同样的命令先在那边跑一遍,DENY 的作用面会当场显出来,停掉的是 staging 的 producer。

相关文章#

参考资料#