没有身份,就没有租户
13.1 的配额绑定在 client.id 或 user 上。client.id 是客户端自己填的,改一个字符串就能绕过;user 来自 SASL 认证,客户端改不了。所以配额要真正生效,前提是先有认证。
认证(authentication)回答「你是谁」,授权(authorization)回答「你能做什么」,加密(encryption)回答「别人能不能在链路上看见」。三件事经常被混成一句「上 SSL 就安全了」,其实它们互相独立:
| 目标 | 手段 | 不做会怎样 |
|---|---|---|
| 证明身份 | SASL(SCRAM / PLAIN / GSSAPI / OAUTHBEARER) | 任何人可以冒充任何 client-id |
| 限制权限 | ACL(StandardAuthorizer) | 认证通过的用户能读写所有主题 |
| 防窃听/篡改 | TLS(SSL / SASL_SSL) | 密码与消息明文过网 |
本节的实验把前两件做实:SCRAM 认证 + ACL 授权。加密只做代价测量,因为 TLS 的配置量大、和业务可靠性关系较弱,讲清成本比堆配置更有用。
SASL 机制怎么选
Kafka 支持四种常用机制,差别在「密码放在哪、走几次往返」:
| 机制 | 凭据存放 | 明文过网 | 往返 | 适用 |
|---|---|---|---|---|
| PLAIN | broker 配置里的用户表 | 是(必须配 TLS) | 1 | 内网、已有统一密码源 |
| SCRAM-SHA-256 | 集群元数据(加盐迭代) | 否(挑战应答) | 2 | 大多数自建集群 |
| SCRAM-SHA-512 | 同上,摘要更强 | 否 | 2 | 合规要求更强摘要 |
| GSSAPI | Kerberos KDC | 否 | 多 | 已有 Kerberos 的企业 |
| OAUTHBEARER | 外部 token 服务 | 否 | 多 | 云上统一身份 |
PLAIN 的密码在 broker 的 listener.name.<listener>.plain.sasl.jaas.config 里以明文列出,改一个用户要改配置并滚动重启;SCRAM 的用户存在集群元数据里,用 kafka-configs.sh 动态增删,不需要重启。这也是本节选 SCRAM 的原因:它把凭据变成了可运维的资源。
SCRAM 的代价是多一次往返,以及每个用户一条 SCRAM-SHA-256=iterations=4096 的元数据记录。4096 是默认迭代次数,可以在创建用户时用 iterations= 调整;调高会增加每次登录的 CPU,不会增加每条消息的开销。
ACL 的粒度
ACL 是一条五元组:principal + host + operation + resource + pattern。资源类型与常用操作:
| 资源 | 常用操作 | 说明 |
|---|---|---|
topic | Read / Write / Describe / Delete | Read 隐含 Describe |
group | Read | 消费者提交位移、加入组都需要 |
cluster | Alter / Describe | 平台管理员才给 |
transactional-id | Write / Describe | 事务生产者需要 |
pattern 只有两种:LITERAL(精确匹配)和 PREFIXED(前缀匹配)。前缀 ACL 是平台治理的关键:给租户一条 tenantA. 的 Read,新建主题不用逐条授权。代价是前缀 ACL 很容易给宽,Read 加在空前缀上等于全开。
另外两个开关决定了 ACL 的默认行为:
authorizer.class.name=org.apache.kafka.metadata.authorizer.StandardAuthorizer:启用授权器。allow.everyone.if.no.acl.found=false:没有匹配 ACL 时拒绝。默认是true,也就是「没配 ACL 就放行」,很多人以为配了 ACL 就安全了,其实默认是全开。
还有 super.users,它绕过所有 ACL 检查。超级用户必须只留给平台账号,包括你自己用来 bootstrap 的那个 principal。
本机实测:单节点 SCRAM + ACL
环境:apache/kafka:4.3.0 单节点 KRaft,四个 listener:内部 PLAINTEXT(9092)、controller(9093)、SASL_PLAINTEXT(19094)、SSL(19095)。SASL 与 SSL 端口直接映射到宿主同名端口,这样容器内外都能用同一份 advertised 地址。
启动时踩到的三个坑
坑一:SASL listener 要求 KAFKA_OPTS。 镜像的 /etc/kafka/docker/configure 在 advertised listeners 里出现 SASL_* 时会 ensure KAFKA_OPTS,并检查其中是否包含 java.security.auth.login.config。我第一次启动容器直接 Exited (1),docker logs 只有一段「0 errors」的汇总块,看不到原因;对照脚本补上 JAAS 文件后才起来:
KAFKA_OPTS='-Djava.security.auth.login.config=/cfg/kafka_server_jaas.conf'
JAAS 文件本身可以是最小化的,因为每个 listener 的 sasl.jaas.config 会覆盖它:
KafkaServer {
org.apache.kafka.common.security.scram.ScramLoginModule required;
};
坑二:listener 级的 JAAS 配置要写对属性名。 属性是 listener.name.sasl.scram-sha-256.sasl.jaas.config。用环境变量表达时,镜像的规则是 _ 变 .、__ 变 _,所以必须保留 SCRAM-SHA-256 里的连字符:
-e 'KAFKA_LISTENER_NAME_SASL_SCRAM-SHA-256_SASL_JAAS_CONFIG=org.apache.kafka.common.security.scram.ScramLoginModule required;'
启动后检查渲染结果:
24:listener.name.sasl.scram-sha-256.sasl.jaas.config=org.apache.kafka.common.security.scram.ScramLoginModule required;
如果我按习惯写成 SCRAM_SHA_256,属性会变成 listener.name.sasl.scram.sha.256.sasl.jaas.config,broker 不认识,SASL 握手直接失败。
坑三:advertised 里有 SSL:// 就强制要 keystore 凭据文件。 脚本会把 KAFKA_SSL_KEYSTORE_LOCATION 覆盖成 /etc/kafka/secrets/<FILENAME>,并要求 KAFKA_SSL_KEY_CREDENTIALS、KAFKA_SSL_KEYSTORE_CREDENTIALS 指向两个只含密码的文件。也就是说,自签证书不能藏在任意目录,必须挂到 /etc/kafka/secrets。
先建用户,再收紧授权
顺序很重要:先把 SCRAM 用户建好,再把 allow.everyone.if.no.acl.found 关掉,否则会把自己锁在门外。本实验用内部 PLAINTEXT listener 的 ANONYMOUS principal 做 bootstrap,并把它设为超级用户:
docker exec mq-rel13-auth sh -c 'unset JMX_PORT; \
/opt/kafka/bin/kafka-configs.sh --bootstrap-server 127.0.0.1:9092 \
--alter --entity-type users --entity-name libraryd \
--add-config "SCRAM-SHA-256=[password=libraryd-secret]"'
本机实测输出:
Completed updating config for user libraryd.
SCRAM credential configs for user-principal 'libraryd' are SCRAM-SHA-256=iterations=4096
再建主题和三条 ACL:libraryd 对 library.loan.created 有 Write 和 Read,对消费组 libraryd-consumer 有 Read。
docker exec mq-rel13-auth sh -c 'unset JMX_PORT; cd /opt/kafka/bin; \
./kafka-acls.sh --bootstrap-server 127.0.0.1:9092 --add \
--allow-principal User:libraryd --operation Write --topic library.loan.created'
授权用户:生产与消费都正常
客户端配置只有四行:
security.protocol=SASL_PLAINTEXT
sasl.mechanism=SCRAM-SHA-256
sasl.jaas.config=org.apache.kafka.common.security.scram.ScramLoginModule required username="libraryd" password="libraryd-secret";
生产三条、再用 libraryd-consumer 组读回:
loan.created L-1 B-101
loan.created L-2 B-102
loan.created L-3 B-103
Processed a total of 3 messages
Processed a total of 3 messages 之前的 TimeoutException 是 --timeout-ms 到点退出的正常行为,不是错误。
未授权主题:原始异常
换一个没有 ACL 的用户 intruder,读同一个主题:
WARN [Consumer clientId=console-consumer, groupId=intruder-consumer] The metadata response from the cluster reported a recoverable issue with correlation id 2 : {library.loan.created=TOPIC_AUTHORIZATION_FAILED}
ERROR [Consumer clientId=console-consumer, groupId=intruder-consumer] Topic authorization failed for topics [library.loan.created]
ERROR Error processing message, terminating consumer process:
org.apache.kafka.common.errors.TopicAuthorizationException: Not authorized to access topics: [library.loan.created]
Processed a total of 0 messages
注意两点:报错发生在拉元数据阶段,不是拉消息阶段,所以客户端连一条记录都没拿到;WARN 里是 TOPIC_AUTHORIZATION_FAILED 错误码,ERROR 里才是被包装成的 Java 异常。排查时按错误码检索比按异常类名检索更快。
密码错误:认证失败
把密码改错一个字符,异常换了一层:
ERROR [Consumer clientId=console-consumer, groupId=libraryd-consumer] Connection to node -1 (localhost/127.0.0.1:19094) failed authentication due to: Authentication failed during authentication due to invalid credentials with SASL mechanism SCRAM-SHA-256
org.apache.kafka.common.errors.SaslAuthenticationException: Authentication failed during authentication due to invalid credentials with SASL mechanism SCRAM-SHA-256
这条和上一条的区别是没有进入授权阶段:连接在 SASL 握手里就断了。监控上要分开计数,否则无法区分「密码配错」和「权限没给」。
前缀 ACL:一条规则放行一族主题
给 intruder 加一条前缀读权限,再读同一个主题:
./kafka-acls.sh --bootstrap-server 127.0.0.1:9092 --add \
--allow-principal User:intruder --operation Read \
--topic library. --resource-pattern-type prefixed
loan.created L-1 B-101
loan.created L-2 B-102
loan.created L-3 B-103
Processed a total of 3 messages
同一条命令把 --topic library. 换成 --group intruder- 就是组前缀授权。这里有个操作陷阱:非交互环境删除 ACL 必须加 --force,否则 CLI 会尝试读控制台确认,直接抛 NPE:
Error while executing ACL command: Cannot invoke "java.io.Console.readLine()" because the return value of "java.lang.System.console()" is null
java.lang.NullPointerException: Cannot invoke "java.io.Console.readLine()" because the return value of "java.lang.System.console()" is null
at org.apache.kafka.tools.AclCommand.confirmAction(AclCommand.java:388)
这条不是 Kafka 的 bug,是 docker exec 没有 TTY 时的 CLI 行为。自动化脚本里所有 --remove 都要带 --force。加上 --force 后同一条命令删除成功,--list 里对应的组前缀 ACL 消失。
主题放行、消费组被拒
把组前缀 ACL 删掉后,用同一个用户读同一个主题,但换一个不匹配任何组 ACL 的 foreign-group:
org.apache.kafka.common.errors.GroupAuthorizationException: Not authorized to access group: foreign-group
Processed a total of 0 messages
这条最能说明 ACL 的粒度:topic 的读权限和 group 的读权限是两张独立的票。只给 topic 不给 group,消费者能拿到元数据、能拉消息,但提交位移和加入组会被拒;反过来只给 group 不给 topic,连消息都拿不到。生产上最常见的「消费者起不来」就是漏了 group 那条 ACL。
认证之后,配额才落到了实处
13.1 的配额可以按 user 配,但前提是 user 来自认证。开了 SCRAM 之后,libraryd 就是 broker 认可的 principal,可以这样把 13.1 和 13.2 接起来:
docker exec mq-rel13-auth sh -c 'unset JMX_PORT; \
/opt/kafka/bin/kafka-configs.sh --bootstrap-server 127.0.0.1:9092 \
--alter --entity-type users --entity-name libraryd \
--add-config producer_byte_rate=8192,consumer_byte_rate=16384'
这条配置对「以 libraryd 认证的所有客户端」生效,客户端无论把 client.id 改成什么,都逃不出这个上限。这才是租户级别配额;按 client-id 配的那套,只适合应用内分模块。
顺序上也要注意:先有认证,再有关闭 allow.everyone.if.no.acl.found,最后才是配额。反过来的话,配额还没生效,集群已经对所有匿名客户端开放了。
TLS 的代价:本机三组对照
同一份生产者代码,只换传输层,向同一主题同步发送 200 条 1024 字节消息。PLAINTEXT 基线取自本机另一个同镜像单节点(39192),SSL 与 SASL 走本节 broker 的 19095 / 19094。三轮都只跑一次;容器内 4 vCPU、/proc/loadavg 约 0.4/0.8/1.0,数字只用于看数量级,不作基准。
| 轮次 | 传输 | 首条耗时(含握手) | 稳态 200 条 | 平均单条 |
|---|---|---|---|---|
| 第一轮 | PLAINTEXT | 230 ms | 470 ms | 1.88 ms |
| 第一轮 | SSL | 381 ms | 704 ms | 3.06 ms |
| 第一轮 | SASL/SCRAM | 340 ms | 799 ms | 3.48 ms |
| 第二轮 | PLAINTEXT | 278 ms | 970 ms | 4.33 ms |
| 第二轮 | SSL | 410 ms | 905 ms | 4.01 ms |
| 第二轮 | SASL/SCRAM | 363 ms | 684 ms | 2.95 ms |
第二轮里 SSL 的稳态比 PLAINTEXT 还快,说明稳态差异已经被调度噪声淹没。两轮里唯一稳定的信号是首条耗时:SSL 比 PLAINTEXT 多约 100–150 ms,SASL 多约 85–110 ms,这部分是握手(TLS 1.3 一次往返 + 证书校验,SCRAM 两次往返 + 4096 次迭代)。16 KB 那一轮的 SSL 首条只多了 70 ms,说明握手成本本身也受调度和批处理影响。
把消息放大到 16 KB、发 300 条,让对称加密的 CPU 占比上升:
| 传输 | 首条耗时 | 稳态 300 条 | 平均单条 |
|---|---|---|---|
| PLAINTEXT | 262 ms | 1033 ms | 2.93 ms |
| SSL | 332 ms | 1281 ms | 3.78 ms |
| SASL/SCRAM | 367 ms | 996 ms | 2.81 ms |
这一轮 SSL 的稳态慢了约 24%,但只有一次运行,不能据此下「TLS 慢 24%」的结论。本机能确定的是握手成本,不能确定稳态 CPU 成本;后者要在目标硬件上用 perf 或 JFR 采样,本节不做。
工程上的结论反而简单:
- 连接是长连接,握手成本被摊薄,TLS 的稳态开销通常远小于一次跨机房 RTT。
- 真正贵的是频繁建连:消费者反复 rebalance、生产者频繁重建、健康检查短连接,都会把握手成本放大。
- 需要为 TLS 预留的 CPU 余量,按吞吐而不是按连接数估;具体系数必须实测。
最小权限的落地顺序
- 先认证,后授权:没有 SASL user 的 ACL 只能约束
User:ANONYMOUS,等于没约束。 - 关掉
allow.everyone.if.no.acl.found,并确保super.users只有平台账号。 - 按「主题前缀 + 组前缀」给租户授权,不要逐主题点。
- 生产端至少要
Write+Describe,消费端要Read(topic)+Read(group)。 - 前缀 ACL 定期审计:
kafka-acls.sh --list的输出进版本库,变更走评审。 - 认证失败与授权失败分开监控:前者是配置/密码问题,后者是权限问题,处置完全不同。
本节判据
- PLAIN 和 SCRAM 的本质区别是什么? 答:PLAIN 的密码存在 broker 配置里、明文过网(必须配 TLS);SCRAM 的凭据存在集群元数据里、走挑战应答,可动态增删。
- 只配了 ACL 但没关
allow.everyone.if.no.acl.found,会发生什么? 答:没有匹配 ACL 的请求仍然放行,ACL 形同虚设。 - 未授权读主题和密码错误,异常有什么区别? 答:前者是
TopicAuthorizationException(已通过认证,卡在授权),后者是SaslAuthenticationException(握手阶段就断了)。 - 为什么 topic 有读权限,消费者还是起不来? 答:消费组也需要
ReadACL;缺了会抛GroupAuthorizationException。 - 本机 TLS 实验能得出什么结论? 答:只能确定握手多 100–150 ms;稳态差异在噪声内,不能宣称 TLS 的吞吐损失比例。
阅读导航:上一节 13.1 多租户与配额 | 下一节 13.3 跨集群复制
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。