OPAを採用したアクセス制御システムのPOCに取り組んでみたので、その内容についてまとめておく。
POCの設計や実装は以下のリポジトリで公開しているため、そちらも合わせて参照されたい。
bmf-san/poc-opa-access-control-system
まず、本記事で扱う重要な概念として「権限制御」と「アクセスコントロール」の違いを以下のように定義する。
権限制御(Authorization)
アクセスコントロール(Access Control)
私はSaaSのプロダクト開発に携わっているのだが、権限管理に関する要件の複雑さや難しさに日々頭を悩ましている。
プロダクトを利用する顧客の組織構造、業務フロー、データアクセスパターンが多様化する中、権限管理システムには次のような課題があるように感じている。
複雑な権限要件への対応
拡張性と保守性
柔軟性と一貫性のバランス
上記の課題に対応するためには、権限システムには例えば次のようなアーキテクチャが求められると考えている。
ビジネスロジックとの分離
きめ細かな制御
拡張可能な設計
これらの課題に対する解決策として、Cloud Native Computing Foundation (CNCF) のGraduatedプロジェクトであるOpen Policy Agent(OPA)をアクセス制御システムに採用する案を検討した。
OPAは以下のような特徴を持つポリシーエンジンである。
Policy as Code
宣言的なポリシー記述
サービスからの分離
OPAを採用する優位性として、以下の点が挙げられる。
マイクロサービスとの親和性
豊富な機能
活発なコミュニティ
本システムでは、プロキシベースのアーキテクチャを採用し、以下の主要コンポーネントで構成されている。
C4Container
title OPAアクセス制御システム - コンテナ図
Person(user, "ユーザー", "システムユーザー")
System_Boundary(opa_system, "OPAアクセス制御システム") {
Container(pep, "PEPプロキシ", "Go", "ポリシー適用ポイント<br>ポリシーを適用するリバースプロキシ")
Container(employee_app, "従業員サービス", "Go", "従業員管理サービス")
Container(pdp, "PDP", "Go + OPA", "ポリシー決定ポイント<br>ポリシー評価を担当")
Container(pip, "PIP", "Go", "ポリシー情報ポイント<br>ポリシーデータを担当")
ContainerDb(employee_db, "従業員データベース", "PostgreSQL", "従業員管理データ")
ContainerDb(prp_db, "PRPデータベース", "PostgreSQL", "ポリシー取得ポイント<br>PRPのデータ")
}
Rel(user, pep, "利用", "HTTP")
Rel(pep, employee_app, "プロキシ", "HTTP")
Rel(pep, pdp, "ポリシー評価", "HTTP")
Rel(pdp, pip, "ポリシー情報要求", "HTTP")
Rel(employee_app, employee_db, "読み書き", "SQL")
Rel(pip, prp_db, "読み取り", "SQL")
UpdateLayoutConfig($c4ShapeInRow="2", $c4BoundaryInRow="1")
基本的なリクエストフローは以下の通りである。
sequenceDiagram
participant Client as クライアント
participant PEP as PEPプロキシ (pep.local)
participant Employee as 従業員サービス (employee.local:8083)
participant PDP as PDP (pdp.local:8081)
participant PIP as PIP (pip.local:8082)
participant PRP as PRPデータベース
participant EDB as 従業員データベース
%% リクエストフロー
Client->>+PEP: GET /employees<br>Header: X-User-ID
%% リソース抽出
PEP->>+PEP: リソース情報抽出<br>type=employees
%% アクセス制御チェックとデータフィルタリング
PEP->>+Employee: プロキシ GET /employees
Employee->>+EDB: 従業員データ照会
EDB-->>-Employee: 全データ返却
Employee-->>-PEP: 200 OK とデータ返却
PEP->>+PDP: POST /evaluation<br>{userID, resourceType, action, data}
PDP->>+PIP: GET /userinfo/{user_id}
PIP->>+PRP: ユーザーロールと権限を照会
PRP-->>-PIP: ロールデータ返却
Note over PDP: アクセス評価とデータフィルタリング
PIP-->>-PDP: ユーザーコンテキスト返却
PDP-->>-PEP: {allow, filtered_data}を返却
alt 許可された場合
PEP-->>-Client: フィルタリング済みデータを返却
else 拒否された場合
PEP-->>Client: 403 Forbiddenを返却
end
PEP(Policy Enforcement Point)の実装には、大きく分けて以下のパターンが考えられる。
プロキシベースの実装
ライブラリベースの実装
サイドカーパターン
APIゲートウェイ統合
本PoCでは、以下の理由からプロキシベースの実装を選択した。
プロキシベースの実装は以下のような処理を行う:
func (p *Proxy) handleRequest(w http.ResponseWriter, r *http.Request) {
// ユーザーIDの取得
userID := r.Header.Get("X-User-ID")
if userID == "" {
http.Error(w, "X-User-ID header is required", http.StatusBadRequest)
return
}
// リソースとアクションの特定
resource := extractResource(r.URL.Path)
action := "view" // 本PoCではGETメソッドのみサポート
// PDPでアクセス評価
allowed, filteredData, err := p.evaluateAccess(userID, resource, action)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if !allowed {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
// プロキシ転送とレスポンスフィルタリング
response := p.forwardRequest(r)
filteredResponse := p.applyFiltering(response, filteredData)
w.Write(filteredResponse)
}
OPAは柔軟なポリシーエンジンであり、様々なアクセス制御モデルを実装できる。
RBAC (Role-Based Access Control)
ABAC (Attribute-Based Access Control)
ReBAC (Relationship-Based Access Control)
その他のモデル
ポリシー定義には大きく2つのアプローチがある。
事後フィルタリング方式
# 本PoCでの実装例:データ取得後にフィルタリング
allowed_fields[field] {
roles := user_roles[input.user_id]
some role in roles
field_permissions := role_field_permissions[role]
field = field_permissions[_]
}
事前フィルタリング方式
# クエリ生成例:データ取得前にフィルタリング
generate_sql_query {
roles := user_roles[input.user_id]
allowed_fields := get_allowed_fields(roles)
query := sprintf("SELECT %s FROM employees WHERE %s",
[concat(", ", allowed_fields), build_conditions(roles)])
}
事後フィルタリング(本PoCの実装)
事前フィルタリング
選択の指針としては以下のような観点がある。
本PoCではRBACモデルを採用し、以下のようなポリシーを実装している。
# ex.
package rbac
# デフォルトで拒否
default allow = false
# アクセス許可ルール
allow {
# ユーザーのロールを取得
roles := user_roles[input.user_id]
# リソースとアクションの権限をチェック
some role in roles
permissions := role_permissions[role]
some permission in permissions
permission.resource == input.resource
permission.action == input.action
}
# フィールドレベルのフィルタリング
allowed_fields[field] {
roles := user_roles[input.user_id]
some role in roles
field_permissions := role_field_permissions[role]
field = field_permissions[_]
}
このポリシーにより、以下が実現される。
PostgreSQLを使用して以下のようなスキーマを実装している。
-- PRPデータベース
CREATE TABLE roles (
id UUID PRIMARY KEY,
name VARCHAR(255) NOT NULL
);
CREATE TABLE users (
id UUID PRIMARY KEY,
name VARCHAR(255) NOT NULL
);
CREATE TABLE user_roles (
user_id UUID REFERENCES users(id),
role_id UUID REFERENCES roles(id),
PRIMARY KEY (user_id, role_id)
);
CREATE TABLE role_permissions (
role_id UUID REFERENCES roles(id),
resource_id UUID REFERENCES resources(id),
action_id UUID REFERENCES actions(id),
PRIMARY KEY (role_id, resource_id, action_id)
);
このスキーマにより、以下が可能となる。
ポリシーとアプリケーションの分離
宣言的なポリシー記述
高い柔軟性
学習コスト
パフォーマンスへの影響
運用の複雑さ
実装上の課題を解決していくために、例えば次のような工夫が求められる。
パフォーマンス最適化
エラーハンドリング
テスト容易性
本システムを通じて、OPAを使用したアクセス制御システムが以下の点で有効であることが確認できた。
アクセス制御の一貫性
開発効率の向上
運用面での利点
一方で、学習コストやインフラの複雑化など、考慮すべき課題も明確になった。これらの課題に対しては、適切な教育とツール整備、そして段階的な導入アプローチが重要となる考えられる。
権限管理システムで最も重要なロジックは、ポリシーの記述とその実装であると思うが、その部分をOPAで実装することで、柔軟性と保守性を高めることができると感じた。
特に、ビジネスロジックの分離ができることで、アクセス制御の変更がアプリケーションコードに影響を与えないという点は大きなメリットであると感じる。
今回はプロキシベースの構成としたが、プロキシの責務が重くなるため、クライアントベースの構成を検討するほうがよりスケーラブルであると思う。
ポリシーとアプリケーションコードが密結合であると、権限システムを開発するチームと権限システムを利用するシステムを開発するチームのコミュニケーションコストが高くなる。プロダクトの成長に伴い権限管理の要件にも柔軟な変化が求められる場合、この点は特に重要であると感じている。
また、ポリシーのテストが容易であることも、ポリシーの品質を高める上で重要であると感じた。ポリシーのテストを自動化することで、ポリシーの変更に伴うリスクを低減することができる。
今回はパフォーマンスを最適化するような工夫は行っていないが、大規模なシステムでOPAを採用する場合は、ポリシーの評価結果のキャッシュやデータベースクエリの最適化など、パフォーマンスに配慮した実装やポリシーの設計が求められると考えられる。
OPAのようなポリシーエンジンを採用していない既存システムからの移行については、ポリシーの抽出と分離、そして段階的な導入が重要であるかつ難しい課題であると感じた。