Hãy tưởng tượng bạn gắn hệ thống thẻ từ nghiêm ngặt cho cả toà nhà văn phòng. Ý định là chặn người lạ. Nhưng bật lên xong mới phát hiện đội lao công, anh giao thư, và cả con robot quét hành lang — những chủ thể nội bộ chẳng ai nghĩ tới — cũng bị chặn vì chưa ai cấp thẻ cho họ. Tệ hơn, khi một người bị từ chối, đầu đọc thẻ kêu "TẦNG BỊ CHẶN" trong khi thứ họ thật sự thiếu là một chiếc chìa khoá phòng riêng. ACL của Kafka cư xử đúng hai kiểu đó, và biết trước thì tiết kiệm được rất nhiều giờ gỡ lỗi.

Xác thực trả lời "bạn là ai". ACL trả lời "bạn được làm gì". Bài này bật ACL trên một cụm thật rồi cấp quyền từng lớp một, và cả hai lỗi đầu tiên đều không nói đúng nguyên nhân.

Bật ACL và broker chết

authorizer.class.name=org.apache.kafka.metadata.authorizer.StandardAuthorizer
super.users=User:admin
allow.everyone.if.no.acl.found=false

Broker không khởi động nổi:

ERROR [ControllerApis nodeId=1] Unexpected error handling request
      apiKey=BROKER_REGISTRATION ... principal=User:ANONYMOUS
RuntimeException: Received a fatal error while waiting for the controller

Nguyên nhân rất cụ thể: broker tự đăng ký với controller qua listener CONTROLLER, và listener đó chạy PLAINTEXT — không có xác thực, nên nó là User:ANONYMOUS. ACL từ chối ANONYMOUS, và broker bị chính luật của nó chặn.

super.users=User:admin;User:ANONYMOUS

Cách đúng hơn cho môi trường thật là cho listener CONTROLLER cũng chạy SASL và cấp cho danh tính đó quyền cụm. Nhưng khi bạn đang dựng lần đầu và broker chết với một thông báo nói về RuntimeException, biết rằng nguyên nhân là ANONYMOUS tiết kiệm rất nhiều thời gian.

Bài học chung: allow.everyone.if.no.acl.found=false áp cho cả lưu lượng nội bộ của Kafka, không chỉ cho client của bạn.

Cấp quyền từng lớp

Bốn bước, mỗi bước một phép đo:

Chưa cấp gì:

ghi -> TopicAuthorizationException
đọc -> TopicAuthorizationException

Cấp Write + Describe trên topic cho user app:

ghi -> THÀNH CÔNG
đọc -> GroupAuthorizationException      <- lớp thứ hai

Ghi chạy được ngay. Đọc thì đổi lỗi — không còn là Topic nữa mà là Group.

Cấp Read + Describe trên topic cho user ana:

đọc -> GroupAuthorizationException      vẫn chưa được

Cấp thêm Read trên nhóm consumer:

đọc -> đọc được 3 tin

Đọc cần hai ACL: Read trên topic và Read trên nhóm consumer. Đây là điều gây bối rối nhất khi mới bật ACL, vì thông báo lỗi ở bước ba không hề nhắc tới topic — bạn vừa cấp quyền topic xong và vẫn bị chặn, nên phản xạ đầu tiên là kiểm lại ACL topic.

Lý do có hai lớp: nhóm consumer là một tài nguyên chung. Không có ACL nhóm thì bất kỳ ai đọc được topic cũng có thể tham gia vào nhóm của người khác và cướp partition của họ.

Producer bị chặn nhận lỗi sai chỗ

User ana chỉ có Read. Thử ghi:

ClusterAuthorizationException: Cluster authorization failed.

Không phải TopicAuthorizationException. Lỗi nói về cụm, và không nhắc tới topic nào.

Nguyên nhân: từ Kafka 3.0, enable.idempotence bật mặc định (phần 6), và producer idempotent cần quyền IdempotentWrite ở mức cụm trước khi làm bất cứ gì. Nó thất bại ở bước đó, trước cả khi tới bước kiểm tra topic.

Người gỡ lỗi đọc "Cluster authorization failed" sẽ đi tìm ACL cụm, trong khi thứ thiếu có thể là ACL topic — hoặc cả hai.

Bộ ACL đầy đủ cho một dịch vụ ghi:

kafka-acls.sh --bootstrap-server ka:9092 --command-config admin.props \
  --add --allow-principal User:app \
  --operation Write --operation Describe --topic don-hang

kafka-acls.sh --bootstrap-server ka:9092 --command-config admin.props \
  --add --allow-principal User:app --operation IdempotentWrite --cluster

Từ Kafka 3.0, Write trên topic ngầm cho IdempotentWrite trong nhiều trường hợp — nhưng khai tường minh thì không bao giờ sai, và nó tránh đúng thông báo lỗi khó hiểu ở trên.

Bộ ACL tối thiểu

Dịch vụ đọc:

--allow-principal User:ana --operation Read --operation Describe --topic don-hang
--allow-principal User:ana --operation Read --group nhom-cua-ana

Dịch vụ ghi:

--allow-principal User:app --operation Write --operation Describe --topic don-hang
--allow-principal User:app --operation IdempotentWrite --cluster

Ứng dụng Kafka Streams cần nhiều hơn cả hai cộng lại: đọc topic nguồn, ghi topic đích, và toàn quyền trên các topic nội bộ nó tự tạo (phần 27). Dùng mẫu tiền tố:

--allow-principal User:streams --operation All \
  --topic <application-id> --resource-pattern-type prefixed

Điều ACL không làm

Describe là mức thấp nhất và nó lộ thông tin. Cấp Describe trên tất cả topic để tiện gỡ lỗi nghĩa là mọi user biết được danh sách topic và cấu trúc của chúng. Đó thường không phải điều bạn muốn.

ACL không giới hạn dung lượng. Một user có Write ghi được bao nhiêu tuỳ thích — đó là việc của hạn ngạch (phần 33). Hai cơ chế này bổ sung cho nhau, không thay thế nhau.

ACL theo client.id không tồn tại. Chỉ có theo principal, tức theo danh tính đã xác thực. Đó là lý do phần 33 nói hạn ngạch theo client.id chỉ là thoả thuận vận hành.

Từ chối thắng cho phép. Một ACL Deny áp cho một principal sẽ thắng mọi ACL Allow khác, kể cả Allow cụ thể hơn. Hữu ích để chặn nhanh một dịch vụ đang gây sự cố, và cũng dễ gây bối rối nếu ai đó thêm Deny rồi quên.

Chuyển đổi an toàn trên cụm đang chạy

Bật allow.everyone.if.no.acl.found=false trên cụm đang chạy sẽ chặn hết mọi thứ trừ những gì đã có ACL. Đó là một sự cố toàn phần trong vài giây.

Quy trình đúng có bốn bước:

  1. Bật authorizer với allow.everyone.if.no.acl.found=true. Mọi thứ vẫn chạy.
  2. Bật ghi nhật ký từ chối (authorizer.logger ở mức DEBUG) và thu thập xem ai đang làm gì.
  3. Thêm ACL cho từng dịch vụ dựa trên dữ liệu thật, không dựa trên tài liệu.
  4. Đổi cờ thành false và theo dõi sát.

Bước 2 là bước hay bị bỏ qua, và nó là bước quan trọng nhất — vì luôn có một dịch vụ mà không ai nhớ là nó đang đọc topic đó.

Cùng một cái bẫy "chính sách ràng buộc cả người trong nhà", đổi ngăn xếp

Hai điều bất ngờ của bài này — broker tự chặn chính mình, và lỗi chỉ sai chỗ cần sửa — không phải đặc sản Kafka. Chúng là hai định luật của mọi hệ phân quyền mặc-định-từ-chối.

Trong thế giới cơ sở dữ liệu, đây đúng là cái bẫy của Row-Level Security: bật một policy hạn chế, rồi phát hiện chính các job migration và tác vụ bảo trì của bạn bị chặn. Chủ bảng vốn bỏ qua RLS theo mặc định, nhưng một khi bạn bật FORCE ROW LEVEL SECURITY thì ngay cả vai quản lý bảng cũng bị chính policy của nó trói — y hệt broker bị luật của mình chặn. Cũng giống một firewall default-deny chặn luôn DNS và health-check, không chỉ chặn kẻ tấn công.

Trong thế giới Java, bật một SecurityManager hay siết module JPMS "từ chối mặc định" để an toàn, rồi chính Spring hay Hibernate của bạn gãy vì chúng vẫn dùng reflection mà trước đó được ngầm cho phép. Và lỗi phân quyền thường chỉ nêu lớp trượt đầu tiên: một 403 nói "bị cấm" chứ không liệt kê đủ vai bạn còn thiếu, hệt như GroupAuthorizationException che mất việc bạn còn cần ACL nhóm.

Mẫu số chung: khi bạn bật một chính sách "mặc định từ chối" để phòng người ngoài, nó ràng buộc mọi chủ thể bên trong ranh giới — kể cả bộ máy nội bộ của chính hệ thống — nên thứ gãy đầu tiên thường là việc hệ tự làm với chính nó, chứ không phải client bạn đang nghĩ tới; và vì một quyết định phân quyền là phép và của nhiều lớp kiểm tra độc lập, thông báo lỗi chỉ nêu lớp trượt đầu tiên, không nêu trọn bộ quyền bạn phải có. Nên câu hỏi đúng khi siết phân quyền không phải "tôi đã cấp quyền cho client chưa" mà là "chính sách này ràng buộc những ai — tôi đã liệt kê cả các chủ thể nội bộ như controller, health-check, job bảo trì chưa — và khi nó từ chối, lỗi đang nói tôi thiếu gì hay chỉ nói lớp nào vừa chặn". Đó cũng là lý do bước "bật ghi nhật ký từ chối rồi quan sát dữ liệu thật" quan trọng hơn mọi tài liệu.

Muốn tự kiểm cụm của bạn có bật phân quyền không, chạy thử:

kafka-acls.sh --bootstrap-server kf:9092 --list 2>&1 | head -3

Nếu nó báo không có authorizer nào được cấu hình, mọi client kết nối được đều có toàn quyền: đọc mọi topic, ghi mọi topic, và xoá mọi topic. Với cụm dùng chung nhiều đội, đó là chỗ đáng sửa trước cả TLS — vì nó không tốn hiệu năng gì.

Phần sau đo sao chép giữa hai trung tâm dữ liệu: dữ liệu sang rất nhanh, còn offset thì không.