Open Policy Agent (OPA) による認可アーキテクチャを構築するときの Tips

## この記事の目的

前回の記事では Open Policy Agent (OPA) が認可アーキテクチャにおいてどのように活用されるかを説明しました。

https://qiita.com/caunu-s/items/a11f081811918d176515

本記事では、実際に構築する際気にしてほしい良いポイントをまとめます。

:::note info
こちらの記事は CNCJ: Cloud Native Security Japan LT祭り「アプリケーションレイヤ認可の動向とアーキテクチャ」の後半部分に相当します。
もしご興味あれば、動画も参考にしてみていただければと思います。
https://youtu.be/VyIFE17KXfg?si=hBCNd9zHgF49jpAi&t=5933
:::

## OPA の判定の仕組みとセキュリティ

OPA では

- ポリシーファイル (Rego)
- データファイル (JSON)

を利用します。
Rego には、判定ルールとスキーマ (インタフェース) を記述します。
パッケージ名を宣言することで名前空間を分離 (≒テナント分け) することができます。

![decision-security-1.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1997806/2eff4ceb-2daf-baca-8d98-dcecc4f9bc3e.png)

JSON には、[RFC 8259](https://datatracker.ietf.org/doc/html/rfc8259) 等の記述ルールに従っている限りは、任意の情報を含めることも複数ファイルに分散させることも可能です。

![json.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1997806/08257935-64ee-5fd2-1fed-73a0938331c5.png)

Rego と JSON の記述例を示します。

![decision-security-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1997806/cf455e18-7222-c540-7ddc-6b1fea4132ce.png)

Rego には

- パッケージ ```A```
- メタデータ ```test policy```
- 返却パラメタ (デフォルト返却値有) ```allow```, ```allow_read```

等が記載されています。

Rego では ```.``` 繋ぎでデータ参照します[^1]。
すなわち値参照のための JSON パース処理の組み込みが不要であり、データ参照に関するエラーが起きにくい特徴があるのですが、データのネストが深くなると参照文が長くなったりタイポに気づけなかったりする可能性もあります。[^2]

[^1]: 詳細は [OPA Document Model](https://www.openpolicyagent.org/docs/latest/philosophy/#the-opa-document-model) をご確認ください。

[^2]: データ構造が確定している場合は、データ参照を関数化しておくと良いです。

OPA への入力データを参照するには ```input.``` データの値を参照するには ```data.``` を使います。
ポリシーからデータを参照することで Rego と JSON が紐づくことになります。

続いて、アプリ・PEP から OPA へのリクエストを考えます。

![decision-security-3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1997806/8c2a13a7-40b8-d5ba-3513-7d62aad326ce.png)

アプリ・PEP は OPA へリクエストを行う際、クエリとしてパッケージ名と返却してもらいたいパラメタを指定することになります。
すなわち、アプリ・PEP は OPA サーバが提供するインタフェースを事前に知っておく必要があります。
ただし、データの構造や内容については意識する必要はありません。

![decision-security-4.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1997806/11813044-b802-51fd-9264-3c8a37242343.png)

OPA サーバはレスポンスとしてクエリに従って応答を返します。

では、もしクエリで指定しなかった場合はどうなるのでしょうか。

![decision-security-5.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1997806/24117054-9595-f56f-0e73-8d162c435c01.png)

OPA は「デフォルト値を持つパラメタ」「≠で適合とされたルールのパラメタ」「メタデータ」等をすべて返してしまいます。
余計な情報を返せば返すほど、ポリシーを詮索されてルールが見破られてしまうかもしれません。
よって「OPA へのリクエストを行うアプリ・PEP は信用できるものなのか？」「OPA への想定外のリクエストは防げるのか？」が重要になってきます。

![decision-security-6.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1997806/a82b17d3-9b14-c6d0-20e9-ddfe84ccd1c4.png)

PEP は信用できるものとして、クエリ作成を PEP に任せてインタフェース情報はアプリに秘匿する方法があります。
簡単に実装できるため、PEP を利用する場合は検討すると良いと思います。

OPA は mTLS をサポートしています。よって想定外のアプリ・クライアントからのアクセスを OPA で弾く方法もあります。[^3]
もちろん、mTLS を PEP で設定してクライアントを検証しておき、PEP-PDP 間は信用するという方法もあります。
また、OPA の mTLS 設定に頼らずサービスメッシュを使うのが一番楽かもしれません。

[^3]: クライアント認証を実装することもできるのですが、Rego でルールを記載して管理しないといけないため mTLS をおススメします。

またそもそも、境界型セキュリティモデルに基づいて IP 制限などは行っておくことが推奨されます。
このような多層防御を行っておくことで、OPA へのリクエストを保全することを推奨します。

## カスタム OPA サーバの開発

OPA を起動する方法は大きく 3 パターンあります。

| # | 起動・判定実行方法 | ユースケース |
|:-:|:-:|:-:|
| 1 | バイナリを実行してプロセスを起動する。API 呼び出しによってポリシー判定を実行する。| 一般的なユースケース。 |
| 2 | OPA SDK (Go ライブラリ) を利用して、ポリシー判定を処理に組み込んだ OPA サーバを起動する。| 特殊な処理を OPA サーバで実行したい場合。<br>生成 (```go build```)・配布されたバイナリからの実行だけでなく、ソースコードを直接実行 (```go run```) したい場合。|
| 3 | ポリシーをビルドして WebAssembly 化する。WebAssembly の読み込みを行える言語を利用した処理にて判定処理を呼び出す。| バイナリの実行やAPI 呼び出しが難しい場合 (ブラウザなどのエッジ環境)|

本来 OPA サーバは判定のみに注力するように設計したいところです。
例えば、

- 判定に用いるデータに、アプリから送信するもの以外も何かしら差し込みたい
- 判定が行われるタイミングでメール通知を行いたい
- 判定結果の一部はアプリに返却したくない

これらは PEP で処理を行うように拡張すれば、配布されているバイナリから OPA を起動する方法で十分です。

ですが、場合によってはどうしても判定直前・判定直後に OPA サーバ**内**で処理を追加したい要件が出てくるかもしれません。
その場合は OPA SDK (OPA の Go ライブラリ, API) を使うことで、自前の OPA サーバを作成しつつ機能拡張することができます。

```golang: main.go
package main

import (
	"context"
	"fmt"
	"log"
	"io/ioutil"
	"encoding/json"
	"github.com/open-policy-agent/opa/rego"
)

func LoadJson(inputPath string) interface{} {
	byteArray, _ := ioutil.ReadFile(inputPath)
	var input interface{}
	_ = json.Unmarshal(byteArray, &input)
	return input
}

func main() {
	ctx := context.Background()
	input := LoadJson("./input-sample/example-input.json")

	r := rego.New(
		rego.Query(`data`),
		rego.LoadBundle("./bundle-sample/bundle.tar.gz"),
	)
	
	query, err := r.PrepareForEval(ctx)
	if err != nil {
		log.Fatal(err)
	}

	rs, err := query.Eval(ctx, rego.EvalInput(input))
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(rs)
}
```

## ポリシー・データの更新

OPA のポリシー・データを更新する方法も複数あります。

![pap-1.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1997806/d56ba4b4-7982-91e5-1196-a1efc2814be9.png)

コードを管理している Git サーバから CD (Continuous Deployment) を行い更新する方法が最も単純に思います。
但し、この方法ではポリシーとデータいずれかの更新を行うごとにデプロイしなければならない負担があり、リアルタイム性にも欠けます。
もちろん、無停止で更新するための基盤設計・更新手順も考えておく必要があります。

![pap-2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1997806/c5e49e5e-e5bf-0e47-95b2-ecb114e7b999.png)

OPA にはポリシー・データを固めたバンドルを公開する HTTP サーバ (ポリシー公開サーバ) にポーリングを行う機能があります。
この方法を使えば Git サーバからポリシー公開サーバへ CD を行うだけで、OPA サーバは無停止・デプロイなしで更新まで行うことができます。
この構成でも、更新は Git サーバ上のパイプラインおよびポーリングの頻度設定に依存し、リアルタイム性に欠けています。

![pap-3.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1997806/a6d79eae-0959-ef18-a273-9e1ef1591ec4.png)

Permit.io による PAP 用 OSS [Open Policy Administration Layer (OPAL)](https://github.com/permitio/opal) を用いると、よりリアルタイム性を追求することができます。
OPAL Server と OPAL Client を用いて Git サーバから OPA にポリシー直接更新をかけるほか、アプリから常に更新がかけられている外部サービスからのデータを取り込むことができます。

例えば

- ユーザの課金情報
- ユーザの所属情報

が入った (頻度高く更新される) 外部データベース内のデータを OPAL で取得してくることができます。
但し、構成が複雑になっていくことによって基盤の管理が煩雑になることは免れません。

OPAL の概要については以下の記事で細かく説明が行われています。

https://qiita.com/ryysud/items/e1743274260b633ed3ce

![pap-4.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1997806/b363a249-af3e-167f-d17c-986b41352c6e.png)

どれくらいリアルタイム性のある認可を行いたいか、どれくらい複雑にしても管理業務として成り立つのかを考慮して検討してみてください。

## Rego 開発補助

OPA はインタフェースの設計が重要であることはここまでの説明で感じていただけたと思います。
その一方でインタフェース定義を行い、かつルールを定義する Rego の記述方法が難しいという意見が良く見つかります。
Rego は宣言型言語であり、苦手意識を持たれる方も多いようです。

対策として、ポリシー記述に先立って事前に標準化を行っておくのはもちろんのこと、ローカル開発や CI (Continuous Integration) で検知する方法が役に立つかもしれません。

Rego リンターである [Regal](https://github.com/StyraInc/regal) や OPA が提供するユニットテスト機能を活用できます。

![support.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1997806/4593aade-4030-9004-70c9-49b194c7fd98.png)

また、生成 AI を用いて Rego の作成できる可能性もあります。
簡単なポリシーの生成を試してみたところ、問題なく生成できているようです。

![generative-ai.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1997806/ae436217-1f40-f58e-3376-4f41f52f3eb2.png)

## 最後に

本記事では OPA の認可アーキテクチャ適用に際して、事前に検討しておきたいことを抜粋してご紹介しました。
特に、誰 (サーバ) が何を担わなくてはならないのか、その間の通信はどうあるべきなのかを事前に練っておく必要があります。

皆さんの今後の OPA 活用のご参考になれば幸いです。