升級到 Pulsar3.0 後深入瞭解 JWT 鑑權

2023-11-20 12:05:44

背景

最近在測試將 Pulsar 2.11.2 升級到 3.0.1的過程中碰到一個鑑權問題,正好藉著這個問題充分了解下 Pulsar 的鑑權機制是如何運轉的。

Pulsar 支援 Namespace/Topic 級別的鑑權,在生產環境中往往會使用 topic 級別的鑑權,從而防止訊息洩露或者其他因為許可權管控不嚴格而導致的問題。

我們會在建立 topic 的時候為 topic 繫結一個應用,這樣就只能由這個應用傳送訊息,其他的應用嘗試傳送訊息的時候會遇到 401 鑑權的異常。

同理,對於訂閱者也可以關聯指定的應用,從而使得只有規定的應用可以消費訊息。

鑑權流程

以上的兩個功能本質上都是通過 Pulsaradmin-API 實現的。


這裡關鍵的就是 role,在我們的場景下通常是一個應用的 AppId,只要是一個和專案唯一系結的 ID 即可。

這只是授權的一步,整個鑑權流程圖如下:

詳細步驟

生成公私鑰

bin/pulsar tokens create-key-pair --output-private-key my-private.key --output-public-key my-public.key

將公鑰分發到 broker 的節點上,鑑權的時候 broker 會使用公鑰進行驗證。

而私鑰通常是管理員單獨儲存起來用於在後續的步驟為使用者端生成 token

使用私鑰生成 token

之後我們便可以使用這個私鑰生成 token 了:

bin/pulsar tokens create --private-key file:///path/to/my-private.key \
            --subject 123456

eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJhZG1pbiJ9

其中的 subject 和本文長提到的 role 相等

使用 subject 授權

只是單純生成了 token 其實並沒有什麼作用,還得將 subject(role) 與 topic 進行授權繫結。


也就是上圖的這個步驟。

這裡建立的 admin 使用者端也得使用一個 superRole 角色的 token 才有許可權進行授權。
superRole 使用在 broker.conf 中進行設定。

使用者端使用 token 接入 broker

PulsarClient client = PulsarClient.builder()
    .serviceUrl("pulsar://broker.example.com:6650/")
    .authentication(AuthenticationFactory.token("eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJKb2UifQ.ipevRNuRP6HflG8cFKnmUPtypruRC4fb1DWtoLL62SY"))
    .build();

使用剛才私鑰生成的 token 接入 broker 才能生產或者消費資料。

originalPrincipal cannot be a proxy role

這些流程正常都沒啥問題,但直到我升級了 Pulsar3.0 後用戶端直接就連不上了。

broker 中看到了 WARN 的警告紀錄檔:

cannot specify originalPrincipal when connecting without valid proxy role


之後在 3.0 的升級紀錄檔中看到相關的 Issue

從這個 PR 相關的程式碼和變更的檔案可以得知:

升級到 3.0 之後風險校驗等級提高了,proxyRole 這個欄位需要在 broker 中進行指定(之前的版本不需要強制填寫)。

因為我們使用了 Proxy 元件,所有的請求都需要從 proxy 中轉一次,這個 proxyRole 是為了告訴 broker:只有使用了 proxyRole 作為 tokenProxy 才能存取 broker,這樣保證了 broker 的安全。

superUserRoles: broker-admin,admin,proxy-admin 
proxyRoles: proxy-admin

以上是我的設定,我的 Proxy 設定的也是 proxy-admin 這個 token,所以理論上是沒有問題的,但依然鑑權失敗了,檢視 broker 的紀錄檔後拿到以下紀錄檔:

Illegal combination of role [proxy-admin] and originalPrincipal [proxy-admin]: originalPrincipal cannot be a proxy role.

排查了許久依然沒有太多頭緒,所以我提了相關的 issue:
https://github.com/apache/pulsar/issues/21583
之後我諮詢了 Pulsar 的 PMC @Technoboy 在他的提示下發現我在測試的時候使用的是 proxy-admin,正好和 proxyRoles 相等。

閱讀原始碼和這個 PRcomment 之後得知:

也就是說使用者端不能使用和 proxyRole 相同的角色進行連線,這個角色應當也只能給 Proxy 使用,這樣的安全性才會高。

所以這個 Comment 還在討論這是一個 breaking change? 還是一個增強修補程式。
因為合併這個 PR 後對沒有使用 proxyRole 的使用者端將無法連線,同時也可能出現我這種 proxyRole 就是使用者端使用的角色,這種情況也會鑑權失敗。

所以我換了一個 superRole 角色就可以了,比如換成了 admin

但其實即便是放到我們的生產系統,只要設定了 proxyRole 也不會有問題,因為我們應用所使用的 role 都是不這裡的 superUserRole,全部都是使用 AppId 生成的。

token 不一致

但也有一個疑惑,我在換為存放在 configmap 中的 admin token 之前(測試環境使用的是 helm 安裝叢集,所以這些 token 都是存放在 configmap 中的),

為了驗證是否只要非 proxyRolesuperRole 都可以使用,我就自己使用了私鑰重新生成了一個 admintoken

bin/pulsar tokens create --private-key file:///pulsar/private/private.key --subject admin

這樣生成的 token 也是可以使用的,但是我將 token 複製出來之後卻發現 helm 生成的 token 與我用 pulsar 命令列生成的 token 並不相同。

為了搞清楚為什麼 token 不同但鑑權依然可以通過的原因,之後我將 token decode之後知道了原因:


原來是 Header 不同從而導致最終的 token 不同,helm 生成的 token 中多了一個 typ 欄位。


之後我檢查了 helm 安裝的流程,發現原來 helm 的指令碼中使用的並不是 Java 的命令列工具:

${PULSARCTL_BIN} token create -a RS256 --private-key-file ${privatekeytmpfile} --subject ${role} 2&> ${tokentmpfile}

這個 PULSARCTL_BIN 是一個由 Go 寫的命令列工具,我檢視了其中的原始碼,才知道 Go 的 JWT 工具會自帶一個 header。
https://github.com/streamnative/pulsarctl


Java 是沒有這個邏輯的,但也只是加了 headerpayload 的值都是相同的。
這樣也就解釋了為什麼 token 不同但確依然能使用的原因。