文章摘要

本文整理自 2022 年 8 月 Apache Pulsar Meetup 上傅腾题为《Apache Pulsar 企业级安全实践》的分享。数据安全已经成为企业的一项重要竞争优势。傅腾针对集群环境分享 Apache Pulsar 的认证和授权实现,讲述企业如何构建安全的 Apache Pulsar 集群并打造满足个性化要求的 Pulsar 认证或授权插件,以满足不同的企业安全需求。

作者介绍

傅腾,StreamNative 技术支持工程师,有着 9 年电信运营商行业大数据相关经验,熟悉大数据的平台架构、建设、安全和维护,对实时数仓、云原生和 AI 具有平台类产品建设经验。

多种安全组合

Apache Pulsar 提供了多种安全组合,包括认证(支持全链路组件认证)、授权(ACL)、传输加密(TLS 和 mTLS)以及端到端加密(仅生产者和消费者可加解密数据),企业可根据实际环境的安全需求来搭配使用。

全链路可信

技术探究|Apache Pulsar 认证与鉴权实践指南_大数据

上图为全链路可信场景。该场景通常见于开放集群或内部测试、功能验证等。该场景中 Broker、Bookie 均开放,生产者明文写入数据。 

内网可信

技术探究|Apache Pulsar 认证与鉴权实践指南_分布式_02

上面两图皆为内网可信场景。其中,第一种设计会做传输层加密和 Broker 认证授权,内网依旧开放。第二种设计将传输层加密放在外部与 Proxy 之间,Proxy 也要做认证。

技术探究|Apache Pulsar 认证与鉴权实践指南_linux_03

上图为内网可信场景中的第三种设计,即常见的负载均衡场景,外部到负载均衡器做传输层加密,后者将数据交给需要认证的 Proxy,再流向 Broker。

内网不可信

技术探究|Apache Pulsar 认证与鉴权实践指南_分布式_04

在内网不可信场景中,内部节点可能因为各种原因有安全隐患,这就需要在每一传输层都开启加密和认证,在 Broker 开启授权。该场景常见于企业跨部门协作、与子公司协作、与外部沟通等情况。

服务不可信(端到端加密)

技术探究|Apache Pulsar 认证与鉴权实践指南_linux_05

服务不可信场景一般是指第三方(如云厂商)提供 Pulsar 服务,为此所有链路都要端到端加密,且只有生产者和消费者才能获取原始数据。

易扩展的安全框架

Apache Pulsar 有着简洁、非常容易扩展的安全框架。企业可方便地自定义安全插件,包括可插拔式的认证和授权、易集成的代码结构、同时支持多种认证的认证链、认证缓存和检测机制。

对于可插拔式的认证和授权:

  • • Pulsar 服务端(如 Broker、Proxy)会对客户端做身份认证,并记录一个 role 作为后续授权的客户端身份标识。该 role 可视为用户实际登录的 token。
  • • 默认支持的认证插件:TLS、Athenz、Kerberos、JWT、OAuth2、Basic。
  • • 支持链式认证,同时支持多个认证源。服务端会将所有 Provider 缓存到本地并初始化,根据客户端传来的标识类型寻找对应的 Provider 做认证。链式认证在云环境中很常见。配置 authenticationProviders:
authenticationProviders=org.apache.pulsar.broker.authentication.AuthenticationProviderToken,org.apache.pulsar.broker.authentication.AuthenticationProviderBasic
  • • Pulsar 支持两种鉴权插件:
  • • AuthorizationProvider:
authorizationProvider=org.apache.pulsar.broker.authorization.AuthenticationProviderToken
  • • MultiRolesTokenAuthorizationProvider,仅适用于认证插件是 JWT 的认证方式:
authorizationProvider=org.apache.pulsar.broker.authorization.MultiRolesTokenAuthorizationProvider

技术探究|Apache Pulsar 认证与鉴权实践指南_java_06

认证的触发时机发生在客户端建立连接时:

1.客户端将一个 CommandConnect 发送给 Broker;

2.AuthenticationService 从 CommandConnect 中可以获取认证方法类型(AuthMethodName);

3.AuthenticationService 再根据缓存的认证插件,调用 authenticate 方法进行认证。

4.注意:缓存认证凭证和认证的过期检查(AuthChallenge,默认值 authenticationRefreshCheckSeconds=60s)

技术探究|Apache Pulsar 认证与鉴权实践指南_编程语言_07

Pulsar 的权限体系中,支持的鉴权级别分别是:

  • • Broker,支持 superuser;
  • • Tenant,支持 tenant admin;
  • • Namespace,支持生产、消费和 Functions;
  • • Topics,支持生产和消费。

插件实现有几点需要注意:initialize() 可以获取 Broker 级别的配置信息和管理元数据;每次鉴权需要先检查传进来的用户是否是 superuser 和 tenant admin,再检查具体的权限。还要注意,只有 superuser 或者 tenant admin 才能为用户赋权。

案例介绍

案例一:使用 JWT 做认证

JSON Web Token(RFC-7519)是 Web 服务中常用的一种认证方案,简称 JWT 认证。JWT 基于一个 token 字符串来识别用户身份。该 token 有着严格的三层结构:

技术探究|Apache Pulsar 认证与鉴权实践指南_编程语言_08

  • • Header 层:JSON 串,用于指定签名算法(Pulsar 中默认使用 HS256),使用 base64url 编码。
  • • Payload 层:JSON 串,用于指定用户名标识(Pulsar 中定义为 sub)和过期时间,使用 base64url 编码。
  • • Signature 层:使用特定算法,将 Header 和 Payload 加一个 secret 值编码而来。签名保证了 token 不被中途篡改。

需要注意前两层是透明的,可以反解出来。

基于 JWT 的特性,在 Pulsar 中使用需要注意几点:

1.使用 JWT 可以做认证和授权,但数据仍然处于暴露状态,所以在安全要求较高的环境建议开启 TLS 对数据传输加密(会牺牲一小部分性能),以进一步巩固安全性。

2.JWT 最重要的特性是不需要借助第三方服务(例如 Kerberos 之于 KDC),凭借 JWT 内容本身就能验证 token 是否有效。这也带来一个问题,即 token 一旦签发,在有效期间将会一直有效,无法撤回。因此对于执行某些重要操作的 token,有效期要尽量设置短。

3.Pulsar 支持两种 JWT 加密方式,即使用对称秘钥(secret key)和使用非对称密钥(private/public key),选择其中一个即可。

4.注意 token 的配置容易出错(例如需要操作 pulsar-admin 但仅给了 test-user 的 token),建议配置后可使用 validate 命令做验证。

5.Pulsar Broker 会缓存客户端的认证信息,并会在一个固定时间(默认 60 秒)来检查每个连接的认证是否过期,这个刷新时间可以配置 broker.conf 中的 authenticationRefreshCheckSeconds 参数来自定义。

6.注意 token 参数的配置既可使用字符串形式,也可使用文件形式,可择优选择。

字符串形式
brokerClientAuthenticationParameters={"token":"eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ0ZXN0LXVzZXIifQ.9OHgE9ZUDeBTZs7nSMEFIuGNEX18FLR3qvy8mqxSxXw"}
从文件读取
brokerClientAuthenticationParameters={"file":"///path/to/proxy-token.txt"}

7.使用 bin/pulsar tokens show 命令可以查看 token 的 header 和 payload:

bin/pulsar tokens show -i eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ0ZXN0LXVzZXIiLCJleHAiOjE2NTY3NzYwOTh9.awbp6DreQwUyV8UCkYyOGXCFbfo4ZoV-dofXYTnFXO8

{"alg":"HS256"}
---
{"sub":"test-user","exp":1656776098}

8.使用 bin/pulsar tokens validate 命令可以用 secret key 或者 public key 验证 token:

bin/pulsar tokens validate -pk  my-public.key -i "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJhZG1pbiJ9.ijp-Qw4JDn1aOQbYy4g4YGBbXYIgLA9lCVrnP-heEtPCdDq11_c-9pQdQwc6RdphvlSfoj50qwL5OtmFPysDuF2caSYzSV1kWRWN-tFzrt-04_LRN-vlgb6D06aWubVFJQBC4DyS-INrYqbXETuxpO4PI9lB6lLXo6px-SD5YJzQmcYwi2hmQedEWszlGPDYi_hDG9SeDYmnMpXTtPU3BcjaDcg9fO6PlHdbnLwq2MfByeIj-VS6EVhKUdaG4kU2EJf5uq2591JJAL5HHiuTZRSFD6YbRXuYqQriw4RtnYWSvSeVMMbcL-JzcSJblNbMmIOdiez43MPYFPTB7TMr8g"

{sub=admin}

案例二:使用 Kerberos 做认证

Kerberos 是大数据领域常用的认证方案,以简单易用和稳定性著称。Pulsar 使用 Java 的 JAAS 机制来支持通过 Kerberos 做身份认证。JAAS 中一个用户信息对应一个 section 。对于 Kerberos 认证而言,一个用户信息中最重要的是 principle 和 keytab,现在可以方便地封装到一个 section 里,最后将这些用户信息拼装到 jaas.conf 文件中。

例如:

SectionName {
com.sun.security.auth.module.Krb5LoginModule required
usekeyTab=true
storekey=true
useTicketCache=false
keyTab="/etc/security/keytabs/pulsarbroker.keytab"
principal="broker/localhost@EXAMPLE.COM".
};
AnotherSectionName{
...
};

SectionName 是指定了用户名标识,内部封装了一个 Kerberos 用户信息。Pulsar 使用 Kerberos 认证时,从配置上而言,就是告知进程该以哪个 section 的身份来启动程序。以 broker 为例:

1.在 broker.conf 中指定服务使用的 section 。

2.而 section 信息是在 pulsar_env.sh 中通过指定 jaas.conf 文件来加载获得的。

使用 Kerberos 做认证时有几点需要注意:

1.Broker service principal 的 {hostname} 和 advertisedAddress 保持一致。例如 Broker service 使用的 Principal 是 "broker/172.17.0.7@SNIO",则 advertisedAddress 也必须配置为相同的 IP:172.17.0.7。因为客户端从 TGT Server 获取到的 Broker 服务端 Principal 中的 hostname 部分是 advertisedAddress 定义的 IP,可从 KDC 日志(/var/log/krb5kdc.log)查看到问题。Broker 服务会涉及两个 Principal,一个是 Broker 服务注册的 Principal,另一个是 TGT Server 返回的 Principal。所以如果两个 Principal 内部的 IP 不一致,用户会永远无法访问到服务,毕竟用户一次只能携带一个服务端Principal 信息。

2.服务端 Principal 命名规则注意:

Broker  Principal 命名格式示例:
#正确示范 broker/host1@MY.REALM
#错误示例 broker@MY.REALM pulsarbroker/host1@MY.REALM
  • • 必须包含完整的三部分:service/hostname@REALM。
  • • service 命名部分,pulsar 里建议使用关键字如​​broker​​、​​proxy​​ (其他会报 warn)。
  • • 主机地址部分为了防止多块网卡的 IP 配置问题,均建议使用 hostname。但如果没有 DNS 解析环境,请参考第一条。可使用 IP,保证服务 IP 和 advertisedAddressIP 一致。
  • • REALM 域名建议使用大写,注意不要复制错。

3.在配置 FQDN 解析时,Kerberos 所有主机都需要支持 FQDN 解析(少量机器可在 /etc/hosts 配置所有主机 DNS 信息)。

案例三:自定义一个鉴权插件

这里以一个鉴权插件为例介绍可插拔的认证和授权实践。案例背景是我们需要界面化的鉴权过程,为此选用大数据领域常用的鉴权组件 Ranger 进行界面化权限管理。具体的开发流程是:

1.根据 Pulsar 现有的权限体系,结合 Ranger 的权限结构,将 Pulsar 注册为 Ranger 的一个服务。

2.在鉴权插件实现 Pulsar 鉴权接口,并在鉴权方法(尤其是初始化方法)中创建 Ranger Client 连接 Ranger 服务做初始化操作。每一个鉴权点都要读取配置好的策略信息进行鉴权。

编写 Ranger 服务定义文件时分为几步:

1.定义资源(包括租户、命名空间和 topic)

2.定义权限类型(如生产和消费)

Pulsar 中的鉴权接口代码如下:

@Override
public CompletableFuture<Boolean> allowTopicOperationAsync(TopicName topicName,
String role,
TopicOperation operation,
AuthenticationDataSourceauthData) {
if (log.isDebugEnabled ()) {
log.debug ("'Check allowTopicOperationAsync ({}] on (t}].", operation.name (), topicName);
}
return validateTenantAdminAccess(topicName.getTenant(),role,authData)
.thenCompose(isSuperUserOrAdmin -> {
if (log. isDebugEnabled()) {
log.debug ("Verify if role (} is allowed to (} to topic {}: isSuperUserOrAdmin=(}",
role, operation, topicName, isSuperUserOrAdmin);
}
if (isSuperUserOrAdmin){
return CompletableFuture.completedFuture(true);
} else {
switch (operation) {
case LOOKUP:
case GET STATS:
case GET METADATA:
return canLookupAsync(topicName, role, authData);
case PRODUCE:
return canProduceAsync(topicName, role, authData);
case GET SUBSCRIPTIONS:
case CONSUME:
case SUBSCRIBE:
case UNSUBSCRIBE:
case SKIP:
case EXPIRE MESSAGES:
case PEEK MESSAGES:
case RESET_CURSOR:
case GET_BACKLOG_SIZE:
case SET_REPLICATED_SUBSCRIPTION_STATUS:
case GET_REPLICATED_SUBSCRIPTION_STATUS:
return canConsumeAsync(topicName, role, authData, authData.getSubscription());
case TERMINATE:
case COMPACT:
case OFFLOAD:
case UNLOAD:
case ADD_BUNDLE_RANGE:
case GET_BUNDLE_RANGE:
case DELETE_BUNDLE_RANGE:
return CompletableFuture.completedFuture(false);
default:
return FutureUtil.failedFuture(new IllegalStateException(
"TopicOperation [" + operation.name() + "] is not supported.')) :
}
}
});
}

Ranger 方法的代码如下:

@Override
public CompletableFuture<Boolean> canProduceAsync (TopicName topicName, String role,
AuthenticationDataSource authenticationData) {
CompletableFuture<Boolean> future = new CompletableFuture@();

RangerAccessResourceImpl resource = new RangerAccessResourceImpl();
resource.setValue(KEY_TENANT, topicName.getTenant())
resource.setValue(KEY_NAMESPACE, topicName.getNamespacePortion());
resource.setValue(KEY_TOPIC, topicName.getLocalName().split("-partition-") [0]);
//resource.setValue(KEY_TAG, "*");

RangerAccessRequestImplrequest = new RangerAccessRequestImpl();

request.setAccessType(AuthAction.produce.name());
request.setUser(role);
request-setResource(resource);
request.setAction (AuthAction.produce.name () ) ;

try {
RangerAccessResult result = rangerPlugin. isAccessAllowed (request);
loq-info("request--->{}", request);
log.info("result--->{}", result);

if (result.getIsAllowed()) {
future. complete (true);
} else {
String errMsg = String
.format ("User '%s' doesn't have produce access to %s, matched policy id = %d",
request.getUser(), topicName.toString(), result.getPolicyId());
log-error(errMsg);
future.completeExceptionally (new Exception(errMsg));
}
} catch (Exception e) {
//access allowed in abnormal situation
log.error("User (] encounter exception in (} produce authorization step."
request.getUser(), topicName. toString(), e);
future. complete (true);
}
return future;
}

最终完成效果如下:

技术探究|Apache Pulsar 认证与鉴权实践指南_大数据_09

Q & A

Q:JWT 认证读取本地 token 文件时是否实时?是否需要重启 Proxy 或 Broker?

A:Client 是被动读取 token 文件的。客户端 token 文件变更不影响服务端,即不需要重启。

Q:生产者是否支持在 HTTP 发送消息过程中在连接时认证?

A:Pulsar 使用 Pulsar 协议发送消息,在第一次连接的时候发起认证。对于管理侧如 pulsar-admin 使用 HTTP 协议,而 HTTP 协议每次连接都会发起认证。

Q:对外鉴权时,是否只需配置 Proxy 而不管 Broker?

A:鉴权是由 Broker 来做的,因此需要配置 Broker。

Q:请具体介绍使用 JWT 时 Broker 对客户端认证信息的缓存策略。

A:有线程专门负责周期性检测所缓存的客户端认证是否过期。如果发现过期,就发 auth challenge 命令给客户端,客户端接收到 auth challenge 命令后会读取 token 文件发送给 Broker;如果检测到更新后的 token 文件,认证就能继续通过,Broker 端重新缓存认证信息。如果客户端在下次检查周期内没有返回认证数据,就会关闭连接。