Open Policy Agent – 快速導入 Authz 至 Microservice 架構


大家好我是LINE Taiwan 的軟體工程師 Chang-Yen, Huang,負責的是 SPOT Team(LINE熱點)Server-side 的開發 ,今天要來跟大家介紹一下Open Policy Agent。

為了避免安全性的問題,現在大多數雲端服務裡都會有權限管理(AuthZ)相關的實作。所謂的權限管理,即是通過限制使用者可以操作的資料範圍,來避免用戶誤改到其他不應有權限修改的資料 。而隨著 microservice 的架構越來越普遍,microservice 在實作權限管理的部分很容易會遭遇到挑戰。

當服務比較單純時,實作上或許可以在各自操作呼叫的地方直接用程式碼做判斷。但是當系統架構越來越龐、越來越複雜時,光是一個簡單的邏輯更換,就得翻遍程式碼,修改每一個當初寫權限邏輯的地方做修改,在維護上就造成了許多困擾。身為工程師當然不能容許這麼勞累的事,因此,大型系統通常會將權限管理獨立出來成為一個服務,並讓需要權限控管的服務在執行前都透過該服務來檢查權限。

但即便如此,還是會有一些問題無法避免,例如需要修改權限時要重新編譯做測試、需要外部相依作為判斷依據的資料等等,這就是為什麼,接下來要跟大家介紹的 Open Policy Agent 如此重要。

什麼是Open Policy Agent?

Open Policy Agent(簡稱OPA) 是使用 Go 語言寫的一個非常輕量的 Open Source Policy Engine,他的目的就是解決在雲端環境上的「策略(Policy)」執行問題,他可以在多種環境上運行,包含 Microservice、Kubernetes、CI/CD Pipeline、API gateway 等,讓你得以使用相同的服務來管理雲端系統上所有的 Policy,解決各式各樣的 Policy 判斷情境。

Open Policy Agent如何運作?

我們先來了解一下 OPA 是如何做 decision making 的,請看以下這張圖。

當我們要做決策時,我們會送一個 Request 到 OPA Server,OPA Server 會根據預先載入的 Policy 檔案Data 對收到的 Request Input 做出 Decision,並回傳一個 Json 的 Response 告知結果。

  • Request Input 通常會是需要判斷的資料本身,在送出 Request 時附在 Body 內。
  • Data 則是 OPA 在做判斷時除了 Request Input 以外額外會用到的資料。
  • Policy 檔案就是事先撰寫好的 Policy 規則。
  • 而 Response 結果不一定需要是單純的 true/false,只要是合法的 Json 格式都可以回傳。

實作

先跟各位介紹一下我們團隊的服務 LINE SPOTLINE SPOT 是一個以您所在的位置為起點,把線下的資訊整合到線上的一個全新的服務,在上面可以看到各式店家的優惠資訊,您也可以在上面的店家資訊頁面上傳照片或透過評論留下您的想法,若各位有興趣可以參考另一篇文章

LINE SPOT平台相當仰賴消費者及店家提供資訊,因此權限的控管與設計,也是一大課題,接下來將以LINE SPOT上店家照片為例,示範如何實作權限控管。

假設今天希望針對照片刪除時實施以下規則:

用戶都可以刪除自己在店家上傳的照片,店家管理者可以刪除所有該店家底下的照片

作為後端,我們希望可以在服務執行刪除照片之前,先送一個 Request 到 OPA Server 去詢問該用戶是否有權限刪除照片。這邊我們送進去的 Input Data 格式會是以下的樣子。

{
  "input": {
    "subject": {
      "id": "Brown",
      "type": "user"
    },
    "object": {
      "id": "image-1",
      "type": "image",
      "owner": "Brown",
      "storeId": "LINE Store"
    },
		"action": "delete"
	}
}

input 裡面我們定義了三個 Key:

  • subject 是 user 的資訊
  • object 是照片的資訊,包含照片的擁有(上傳)者及照片屬於哪一個店家
  • action 是希望執行的動作,以這邊的例子來說就是刪除

撰寫Policy File

在定義了 Input 即要實施的規則之後,接下來就需要撰寫 OPA 的 Policy 檔案,Policy 檔案都是用 Rego 寫成的,Rego 源自於 Datalog,是一個專門為了 OPA 而發明的宣告式語言,Rego 相關詳細的文件與使用方式可以參考官網上的說明。

首先,我們可以把上面的規則拆成兩個部分來實作。

Part 1:

用戶都可以刪除自己在店家上傳的照片,店家管理者可以刪除所有該店家底下的照片

這邊先針對第一個部分撰寫規則 並命名該規則為 allow

package example

import input.subject
import input.object
import input.action

default allow = false

allow {
  subject.type == "user"
  object.type == "image"
  action == "delete"
  subject.id == object.owner
}

在這裡我們判斷了 subjectobjectaction 是否符合規則,以及該照片上傳者是否為該用戶,若以上條件皆成立便會回傳 true ,同時在檔案開頭我們也定義了當條件皆不符合時的預設值為 false ,這樣第一條規則便完成了。

Context Data

用戶都可以刪除自己在店家上傳的照片,店家管理者可以刪除所有該店家底下的照片

接下來在第二部分我們會發現,光靠上面 input 的訊息並不足以判斷第二條規則,我們還必須知道店家的管理者有誰。

如何取得這類型的Context Data?OPA裡可用的方式大致上分為四類,建議根據資料量與變化性來彈性運用,以下分別介紹:

  1. Asynchronous Push:透過 OPA 的 API PUT /v1/data 來寫入, 可透過 data 這個 global variable 取得
  2. Asynchronous Pull:透過 OPA 的 Bundle Feature 取得,可透過 data 這個 global variable 取得,Bundle 部分稍後再跟各位講解
  3. Synchronous Push:透過 input 一併傳入
  4. Synchronous Pull:利用內建的 http.send call 外部 API

官方亦提供簡易比較表可參考

確定採用的方式後,我們再回到欲撰寫的規則上。

用戶都可以刪除自己在店家上傳的照片,店家管理者可以刪除所有該店家底下的照片

假設透過 Asynchronous Push 的方式將下列的 Context Data 存入 OPA Server。

{
  "store_admins": {
    "LINE Store": ["Cony", "Sally"],
		"Choco's Store": ["Choco"]
  }
}

針對第二個部分再撰寫一個名稱相同的規則,OPA在執行時即會將所有同名的規則都跑過一遍。

allow {
  subject.type == "user"
  object.type == "image"
  action == "delete"
  admins := data.store_admins[object.storeId]
  subject.id == admins[_]
}

如此一來,可以直接透過 data.store_admins 來取得店家的管理者清單,接下來最後一行用 Rego 的語法可以判斷該清單中是否包含 subject.id ,這樣就完成所有Policy的撰寫了。

Testing

還沒有結束呢!別忘了最重要的步驟:測試。

OPA官方提供了一個Playground給大家玩, 可以自行輸入Input、Data、Policy,看看執行完的Response跟Coverage。

如果你想在 Local 測試,可以直接安裝 OPA 並在 Local 端執行,Mac 的用戶直接透過 brew install opa 就可以了。如果是 Windows 的用戶也可以透過官網提供的方式下載,官方也有 publish Docker image 可以使用。

安裝完執行 opa run --server --watch my_policy_folder 便可直接將OPA Server run 起來並透過打API的方式進行測試。(也可以參考官方提供的測試教學

這裡我們將上面的 Request Input 透過 API 送出 POST /v1/data/example/allow,應該就會拿到以下的結果。

{
  "result": true
}

服務整合OPA 佈署

Policy 撰寫完就要來將 OPA 佈署到我們雲端的 Kubernetes 上了,在佈署上 OPA 有幾種推薦的做法:

1. Go Library 如果你的服務也是用 Go 寫的那恭喜你可以直接將 OPA 當做 Library 來使用

2. Sidecar Container 將 OPA 以 Sidecar 的方式佈署到需要使用的服務中,這樣在乎叫上才不會有 network 的延遲

3. Host-level daemon 將 OPA 佈署在每台 Host 上,這樣也是可以降低跨機器的 network request

記得我們是為了建立一個給所有 microservice 使用的 AuthZ 服務。當然我們可以建立一個 AuthZ 的服務讓所有需要檢查權限的服務先送Request 給 AuthZ 服務,但這樣的缺點就是 network 會影響 performance。

個人比較建議的方式是使用 Sidecar Container 的方式來佈署在所有需要使用到的服務當中來解決 network 的問題,當然這樣同時也會遇到另外一個問題就是該如何在所有的 OPA Server 間同步 Policy 與 Context Data。

為了解決這個問題,這邊提供兩種建議的方式來處理。

1. 用 NFS 來同步檔案

當使用 opa run --server --watch my_policy_folder 執行時,OPA 會偵測該資料夾內是否有檔案更新並動態更新 Policy。因此當我們使用 NFS mount volume 到每台 OPA instance 上的話,只要在一台機器上更新檔案所有 OPA  Server 都會跟著更新。 但這邊有個坑要注意的是,由於 OPA 註冊的 File Event 在 NFS File 新增時並不會觸發到所以如果會有動態新增檔案的需求則比較不建議使用此方式。(當然還是有些workaround)

2. 使用 OPA Bundle Feature

你可以將你的 Policy、Context Data 打包成一個 tar.gz 檔後讓 OPA 自動去更新。這種方式會需要提供一個 Config 檔讓 OPA 知道去哪裡取得檔案以及更新的間隔時間等等。 詳細的設定方式可以參考官方說明,這邊提供一個範例去抓取 S3 上的檔案:

services:
  my_service:
    url: http://example-bucket.s3-website.us-west-2.amazonaws.com
    credentials:
      s3_signing:
        environment_credentials: {}
#          這邊會需要提供以下三個環境變數給OPA
#          AWS_ACCESS_KEY_ID: ********************
#          AWS_SECRET_ACCESS_KEY: *******************
#          AWS_REGION: *************

bundles:
  demo:
    service: my_service
    resource: example/rules/bundle.tar.gz
    persist: true
    polling:
      min_delay_seconds: 10
      max_delay_seconds: 20

使用 opa run –server –config-file config.yaml 啟動 OPA Server 就行了,這時我們可以從 log 看到 Bundle downloaded and activated successfully 就代表成功了。

Conclusion

OPA 可以讓你在將 Policy decouple 的同時保證低延遲與高可靠性,並讓 Policy 可以在不用 recompile redeploy 的情況下做及時更新。上面用一個簡單的例子介紹 OPA 其中一個基本的用法,而OPA本身可以提供的功能實在是非常廣。你可以將 OPA 直接整合在 Kubernetes,或是在 Proxy 上在 access 每個 resource 前作檢查等等各式各樣的應用上,這邊就留給有興趣的各位去鑽研了。

您是否想挑戰大流量平台呢?趕快手刀加入 LINE!

目前招募大門已經打開囉!如果你對於 Server-Side 相關工作有興趣,歡迎參考以下職缺

其他職缺請參考 2021 LINE Taiwan Developers Recruitment Day 系列文章,裡頭將會詳細敘述各個職缺以及相關介紹

關於 TECH FRESH 計畫

2021 年的 TECH FRESH 實習生計畫已經開跑囉!我們正在尋找對技術有熱情、積極解決問題、勇於接受挑戰的優秀同學,馬上手刀申請送出履歷,下一位 TECH FRESH 就是你!

若想更了解關於這個計畫的內容,歡迎讀者們參閱以下兩篇文章,更多的 FAQ 都已經整理在其中: