OPA

Open Policy Agent (OPA) 完全ガイド — アーキテクチャ・機能・設定の全容


1. はじめに

1.1 OPA とは何か

Open Policy Agent (OPA) は、Cloud Native Computing Foundation (CNCF) の卒業プロジェクトとして管理されている、汎用的なポリシーエンジンである。Styra 社が 2016 年に開発を開始し、2021 年に CNCF Graduated ステータスを取得した。OPA は「ポリシーをコードとして管理する (Policy as Code)」というパラダイムを実現するためのオープンソースツールであり、ソフトウェアスタック全体にわたって統一的なポリシーの記述・適用・テストを可能にする。

OPA の核となるアイデアは極めてシンプルである。ポリシーの判断 (decision) をサービスのビジネスロジックから切り離す ということだ。従来、認可ロジックや入力バリデーションはアプリケーションコード内にハードコーディングされることが多かった。しかし、マイクロサービスの普及やクラウドネイティブ環境の複雑化に伴い、各サービスに散在するポリシーロジックを一元管理し、監査可能な形で運用する必要性が増している。

OPA はその課題を解決するために設計されている。Go 言語で実装された軽量なバイナリとして提供され、以下の特徴を持つ。

  • 汎用ポリシーエンジン: 特定のドメイン(例: Kubernetes のみ)に縛られず、API 認可、データフィルタリング、Terraform のプランバリデーション、CI/CD パイプラインなど、あらゆるシステムにポリシーを適用できる
  • Rego 言語: OPA 専用の宣言型ポリシー言語。Datalog をベースにしており、構造化データに対する問い合わせとポリシー判断を簡潔に記述できる
  • デカップリングアーキテクチャ: ポリシーの判断を行う OPA エンジンと、判断を施行 (enforce) するシステムを明確に分離する。これにより、ポリシーの変更がアプリケーションの再デプロイなしに反映される
  • 軽量・高速: インメモリで動作し、マイクロ秒〜ミリ秒オーダーでポリシー評価を返す。サイドカーとしてのデプロイに適している

1.2 OPA が解決する課題

現代のクラウドネイティブアーキテクチャでは、以下のような課題が頻繁に発生する。

課題詳細
ポリシーの散在各マイクロサービスに認可ロジックが分散し、一貫性の確保が困難
変更の遅延ポリシー変更のたびにアプリケーションの再デプロイが必要
監査の困難さポリシーがコードに埋め込まれているため、何がどのように判断されているかを把握しにくい
テスト不足ポリシーロジックのユニットテストが困難で、回帰テストが行われていない
言語・フレームワーク依存Java、Python、Go など言語ごとに異なる認可ライブラリを使用し、統一が困難

OPA はこれらすべてに対して統一的な解を提供する。

1.3 本記事の構成

本記事は以下の構成で OPA の全体像を網羅する。

  1. はじめに (本章)
  2. コアコンセプト — ポリシー、データ、クエリの三位一体
  3. Rego 言語の詳細 — 文法、パターン、実践的なポリシー記述
  4. アーキテクチャと動作原理 — 内部構造と評価エンジン
  5. デプロイメントパターン — サイドカー、デーモン、Go ライブラリ、WASM
  6. Kubernetes との統合 (OPA Gatekeeper) — Admission Control の実践
  7. マイクロサービス API 認可 — HTTP API とサービスメッシュ統合
  8. Envoy / Istio との統合 — External Authorization フィルタ
  9. Terraform との統合 — インフラストラクチャポリシー
  10. ポリシーテスト — ユニットテスト、カバレッジ、CI/CD 統合
  11. バンドルとポリシー配布 — 大規模運用のためのポリシー管理
  12. パフォーマンスチューニング — ベンチマーク、プロファイリング、最適化
  13. セキュリティ考慮事項 — OPA 自体のセキュリティ設定
  14. エコシステムとツール — Conftest, Gatekeeper, Styra DAS など
  15. まとめとベストプラクティス

2. コアコンセプト

OPA を理解するうえで最も重要な概念は、ポリシー (Policy)データ (Data)クエリ (Query) の三つである。これらの関係を正確に理解することが、OPA を効果的に活用するための基盤となる。

2.1 三位一体モデル

OPA の動作は以下のシンプルなモデルで表現できる。

           ┌──────────────┐
           │   Query      │
           │  (Input)     │
           └──────┬───────┘
                  │
                  ▼
     ┌────────────────────────┐
     │                        │
     │   OPA Engine           │
     │                        │
     │  ┌──────────────────┐  │
     │  │   Policy (Rego)  │  │
     │  └──────────────────┘  │
     │  ┌──────────────────┐  │
     │  │   Data (JSON)    │  │
     │  └──────────────────┘  │
     │                        │
     └────────────┬───────────┘
                  │
                  ▼
           ┌──────────────┐
           │  Decision    │
           │  (Output)    │
           └──────────────┘
  • ポリシー (Policy): Rego 言語で記述されたルールの集合。「何が許可されるか」「何が禁止されるか」を宣言的に定義する
  • データ (Data): ポリシー評価に必要な外部情報。JSON 形式で OPA に事前にロードされる。例: ユーザーのロール情報、リソースの所有者情報、組織のコンプライアンス要件など
  • クエリ (Query / Input): ポリシー評価のトリガーとなる入力データ。API リクエストの内容、Kubernetes の Admission Review オブジェクト、Terraform のプランデータなどが該当する

OPA はポリシーとデータをもとにクエリを評価し、判断 (Decision) を返す。判断は JSON 形式で返され、単純な true/false から複雑な構造化データまで対応する。

2.2 Document モデル

OPA は内部的にすべてのデータを ドキュメント (Document) として管理する。ドキュメントは以下の 3 種類に分類される。

2.2.1 Base Document

外部から OPA にロードされるデータ。data ルートの下に格納される。

{
  "data": {
    "users": {
      "alice": {"role": "admin", "department": "engineering"},
      "bob": {"role": "viewer", "department": "marketing"}
    },
    "roles": {
      "admin": {"permissions": ["read", "write", "delete"]},
      "viewer": {"permissions": ["read"]}
    }
  }
}

2.2.2 Virtual Document

Rego ルールによって生成される計算済みのドキュメント。ルールの評価結果として動的に生成される。

package authz

# Virtual Document: data.authz.allow
allow if {
    some role in input.user.roles
    some permission in data.roles[role].permissions
    permission == input.action
}

この例では data.authz.allow が Virtual Document であり、入力と Base Document をもとに true または false を返す。

2.2.3 Input Document

クエリとともに提供されるデータ。input キーワードでアクセスする。

{
  "input": {
    "user": {
      "name": "alice",
      "roles": ["admin"]
    },
    "action": "write",
    "resource": "/api/v1/users"
  }
}

2.3 ポリシーのライフサイクル

OPA におけるポリシーのライフサイクルは以下のとおりである。

  記述 (Author)
      │
      ▼
  テスト (Test)
      │
      ▼
  レビュー (Review)
      │
      ▼
  公開 (Publish)
      │
      ▼
  配布 (Distribute)
      │
      ▼
  適用 (Enforce)
      │
      ▼
  監視 (Monitor)
      │
      ▼
  改善 (Improve)
      │
      └──→ 記述 に戻る
  1. 記述: Rego でポリシーを記述する
  2. テスト: opa test コマンドでユニットテストを実行する
  3. レビュー: Git のプルリクエストを通じてコードレビューを行う
  4. 公開: バンドルとしてパッケージングし、OCI レジストリやオブジェクトストレージに公開する
  5. 配布: OPA のバンドル API を通じて各エージェントにポリシーを配布する
  6. 適用: OPA がポリシーを評価し、判断を返す
  7. 監視: Decision Log を通じて判断結果を監視・監査する
  8. 改善: 監視結果をもとにポリシーを改善する

2.4 ポリシーの分離原則

OPA を効果的に運用するための重要な設計原則として、以下の分離がある。

分離説明
判断と施行の分離OPA はポリシーの判断のみを行い、施行は呼び出し元のシステムが行う
ポリシーとデータの分離ポリシーのルール (Rego) と参照データ (JSON) は別々に管理する
ポリシーとアプリケーションの分離ポリシーのロジックはアプリケーションコードから完全に独立する

この分離により、各コンポーネントが独立してバージョン管理・テスト・デプロイできるようになる。


3. Rego 言語の詳細

3.1 Rego の概要

Rego は OPA 専用の宣言型ポリシー言語であり、名前は「ルールのための言語 (re-go: rules for Go)」に由来する。Datalog をベースに設計されており、JSON/YAML などの構造化データに対して直感的に問い合わせやルール定義を行える。

Rego の主な特徴:

  • 宣言的: 「何が真であるか」を宣言する。手続き的な制御フローは使用しない
  • JSON ネイティブ: 入力・出力ともに JSON 形式。JSON データの操作に最適化されている
  • 完全評価: ルールは完全に評価され、結果がキャッシュされる
  • 部分評価: 入力が不完全な場合でも、既知の情報に基づいて部分的に評価できる

3.2 基本文法

3.2.1 パッケージとインポート

# パッケージ宣言: すべての Rego ファイルはパッケージに属する
package httpapi.authz

# インポート: 他のパッケージやデータを参照する
import data.users
import data.roles
import input.request

パッケージはドット区切りの階層構造を持ち、ファイルシステムのパスに対応する。

3.2.2 ルールの定義

Rego のルールは以下の形式で記述する。

# 定数ルール (Complete Rule)
default allow := false

# 条件付きルール
allow if {
    input.method == "GET"
    input.path == "/public"
}

# 複数の条件を持つルール (AND 条件: すべての行が真である必要がある)
allow if {
    input.user.role == "admin"
    input.method == "DELETE"
    startswith(input.path, "/api/")
}

# 複数のルール定義による OR 条件
allow if {
    input.user.role == "editor"
    input.method == "PUT"
}

重要なポイント:

  • 同一ルール内の複数の式は AND 条件として評価される
  • 同一名の複数ルールは OR 条件として評価される
  • default キーワードは、すべてのルールが undefined の場合のデフォルト値を定義する

3.2.3 変数とイテレーション

# some キーワードによる変数宣言とイテレーション
allow if {
    some i
    input.user.roles[i] == "admin"
}

# より簡潔な形式 (v0.34+)
allow if {
    some role in input.user.roles
    role == "admin"
}

# every キーワード (v0.44+): すべての要素が条件を満たすことを要求
all_users_active if {
    every user in data.users {
        user.status == "active"
    }
}

3.2.4 内包表記 (Comprehension)

# Set 内包表記
admin_users := {user |
    some user in data.users
    user.role == "admin"
}

# Array 内包表記
sorted_names := [name |
    some user in data.users
    name := user.name
]

# Object 内包表記
user_roles := {name: role |
    some name, user in data.users
    role := user.role
}

3.3 データ型

Rego は以下のデータ型をサポートする。

説明
Booleantrue, false真偽値
Number42, 3.14整数・浮動小数点数
String"hello"文字列 (UTF-8)
Nullnullnull 値
Array[1, 2, 3]順序付きリスト
Object{"key": "value"}キーバリューマップ
Set{1, 2, 3}一意な値の集合

3.4 組み込み関数

Rego は豊富な組み込み関数を提供する。以下は主要なカテゴリと代表的な関数である。

文字列操作

# 文字列の結合
result := concat(", ", ["a", "b", "c"])  # "a, b, c"

# 文字列の検索
contains("hello world", "world")  # true
startswith("/api/v1/users", "/api/")  # true

# 正規表現
regex.match("^[a-z]+$", "hello")  # true

# フォーマット
result := sprintf("user: %s, role: %s", [input.user, input.role])

数値操作

max_value := max({1, 2, 3})  # 3
abs_value := abs(-5)  # 5
round_value := round(3.7)  # 4

集合操作

# 和集合
union_set := {1, 2, 3} | {3, 4, 5}  # {1, 2, 3, 4, 5}

# 積集合
intersect_set := {1, 2, 3} & {2, 3, 4}  # {2, 3}

# 差集合
diff_set := {1, 2, 3} - {2}  # {1, 3}

オブジェクト操作

# キーの取得
keys := object.keys({"a": 1, "b": 2})  # {"a", "b"}

# オブジェクトのマージ
merged := object.union({"a": 1}, {"b": 2})  # {"a": 1, "b": 2}

# パスによるアクセス
value := object.get(input, ["user", "name"], "unknown")

JWT / 暗号操作

# JWT の検証とデコード
[header, payload, sig] := io.jwt.decode(input.token)

# 署名付き JWT の検証
io.jwt.verify_rs256(input.token, data.keys.public_key)

# JWT のデコードと検証を同時に行う
[valid, header, payload] := io.jwt.decode_verify(input.token, {
    "cert": data.keys.public_key,
    "iss": "https://auth.example.com",
    "aud": "my-service"
})

HTTP / ネットワーク

# 外部 API の呼び出し (注意: パフォーマンスに影響する可能性あり)
response := http.send({
    "method": "GET",
    "url": "https://api.example.com/users",
    "headers": {"Authorization": concat(" ", ["Bearer", input.token])},
    "raise_error": false
})

# CIDR マッチング
net.cidr_contains("10.0.0.0/8", "10.1.2.3")  # true

3.5 実践的なポリシー例

3.5.1 RBAC (ロールベースアクセス制御)

package rbac

import data.roles
import data.user_roles

default allow := false

# ユーザーのロールに紐づく権限を取得
user_permissions[permission] {
    some role in user_roles[input.user]
    some permission in roles[role].permissions
}

# 要求されたアクションがユーザーの権限に含まれるか確認
allow if {
    required_permission := concat(":", [input.action, input.resource])
    required_permission in user_permissions
}

# 管理者は常にアクセス可能
allow if {
    some role in user_roles[input.user]
    role == "super_admin"
}

対応するデータ:

{
  "roles": {
    "admin": {
      "permissions": ["read:users", "write:users", "delete:users", "read:reports"]
    },
    "editor": {
      "permissions": ["read:users", "write:users", "read:reports"]
    },
    "viewer": {
      "permissions": ["read:users", "read:reports"]
    }
  },
  "user_roles": {
    "alice": ["admin"],
    "bob": ["editor", "viewer"],
    "charlie": ["viewer"]
  }
}

3.5.2 ABAC (属性ベースアクセス制御)

package abac

default allow := false

# 勤務時間内のアクセスのみ許可
allow if {
    input.user.department == input.resource.department
    input.action == "read"
    is_business_hours
}

# 機密リソースはマネージャー以上のみ
allow if {
    input.resource.classification == "confidential"
    input.user.level >= 5
    input.user.department == input.resource.department
}

is_business_hours if {
    now := time.now_ns()
    [hour, _, _] := time.clock([now, "Asia/Tokyo"])
    hour >= 9
    hour < 18
}

3.5.3 API リクエストのバリデーション

package api.validation

# バリデーションエラーの収集
violations[msg] {
    input.request.method == "POST"
    input.request.path == "/api/v1/users"
    not input.request.body.email
    msg := "email フィールドは必須です"
}

violations[msg] {
    input.request.method == "POST"
    input.request.path == "/api/v1/users"
    input.request.body.email
    not regex.match(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`, input.request.body.email)
    msg := "無効なメールアドレス形式です"
}

violations[msg] {
    input.request.method == "POST"
    input.request.path == "/api/v1/users"
    input.request.body.age
    input.request.body.age < 0
    msg := "年齢は 0 以上である必要があります"
}

# バリデーション結果
result := {
    "allow": count(violations) == 0,
    "violations": violations
}

4. アーキテクチャと動作原理

4.1 OPA の内部アーキテクチャ

OPA は以下の主要コンポーネントから構成される。

┌─────────────────────────────────────────────────────┐
│                    OPA Server                        │
│                                                      │
│  ┌─────────┐  ┌─────────────┐  ┌────────────────┐  │
│  │  REST   │  │   Bundle    │  │   Decision     │  │
│  │  API    │  │   Loader    │  │   Logger       │  │
│  └────┬────┘  └──────┬──────┘  └───────┬────────┘  │
│       │              │                 │            │
│  ┌────▼──────────────▼─────────────────▼────────┐  │
│  │              Policy Store                      │  │
│  │  ┌──────────────┐  ┌─────────────────────┐   │  │
│  │  │ Rego Modules │  │  JSON Data Store    │   │  │
│  │  └──────────────┘  └─────────────────────┘   │  │
│  └──────────────────────┬───────────────────────┘  │
│                         │                           │
│  ┌──────────────────────▼───────────────────────┐  │
│  │            Evaluation Engine                   │  │
│  │  ┌──────────┐  ┌────────────┐  ┌───────────┐ │  │
│  │  │  Parser  │  │  Compiler  │  │  Topdown  │ │  │
│  │  │          │  │            │  │  Evaluator│ │  │
│  │  └──────────┘  └────────────┘  └───────────┘ │  │
│  │  ┌──────────┐  ┌────────────┐                │  │
│  │  │  Planner │  │   WASM     │                │  │
│  │  │          │  │  Compiler  │                │  │
│  │  └──────────┘  └────────────┘                │  │
│  └──────────────────────────────────────────────┘  │
│                                                      │
│  ┌──────────────────────────────────────────────┐  │
│  │           Status / Health / Metrics            │  │
│  └──────────────────────────────────────────────┘  │
│                                                      │
└─────────────────────────────────────────────────────┘

4.1.1 REST API

OPA はデフォルトで HTTP サーバーとして動作し、以下のエンドポイントを公開する。

エンドポイントメソッド説明
/v1/data/{path}GET/POSTポリシーの評価 (判断の取得)
/v1/policies/{id}GET/PUT/DELETEポリシーの管理 (CRUD)
/v1/data/{path}PUT/PATCHデータの管理
/v1/queryPOSTアドホッククエリの実行
/v1/compilePOST部分評価 (Partial Evaluation)
/healthGETヘルスチェック
/metricsGETPrometheus メトリクス

4.1.2 評価エンジン

OPA の評価エンジンは以下のステップでポリシーを処理する。

  1. パース (Parse): Rego ソースコードを AST (Abstract Syntax Tree) に変換
  2. コンパイル (Compile): AST を最適化し、型チェックを実行
  3. 評価 (Evaluate): トップダウン方式でルールを評価

トップダウン評価は再帰的に動作し、以下の特性を持つ。

  • メモ化 (Memoization): 一度評価されたルールの結果はキャッシュされ、同一クエリ内で再利用される
  • バックトラッキング: 条件が失敗した場合、前の選択肢に戻って代替パスを試行する
  • コンプリート評価: 結果が undefined になるルールはデフォルト値にフォールバックする

4.1.3 Bundle Loader

Bundle Loader はリモートの HTTP サーバーから定期的にポリシーバンドルをダウンロードする。バンドルは .tar.gz 形式でパッケージングされ、以下を含む。

  • Rego ファイル (.rego)
  • JSON データファイル (data.json)
  • マニフェストファイル (.manifest)
  • オプションの署名ファイル (.signatures.json)

4.1.4 Decision Logger

Decision Logger は OPA の判断結果をログとして外部システムに送信する。これにより、すべてのポリシー判断の監査証跡が記録される。

4.2 データフロー

OPA を中心としたデータフローを時系列で示す。

1. アプリケーションが OPA にクエリを送信
   POST /v1/data/authz/allow
   {"input": {"user": "alice", "action": "read", "resource": "/api/users"}}

2. OPA が入力データを受け取り、ポリシーを評価
   - input.user → "alice"
   - data.user_roles["alice"] → ["admin"]
   - data.roles["admin"].permissions → ["read", "write", "delete"]
   - "read" in permissions → true
   - allow → true

3. OPA が判断結果を返す
   {"result": true}

4. Decision Logger が判断をログに記録
   {
     "decision_id": "abc-123",
     "input": {...},
     "result": true,
     "timestamp": "2025-01-15T10:30:00Z",
     "metrics": {"timer_rego_query_eval_ns": 125000}
   }

5. アプリケーションが判断に基づいてリクエストを許可/拒否

4.3 OPA の設定

OPA サーバーの設定は YAML ファイルで行う。以下は典型的な設定例である。

# opa-config.yaml

# サービス定義: バンドルやステータスの送信先
services:
  - name: bundle-server
    url: https://bundle.example.com
    credentials:
      bearer:
        token: "${BUNDLE_TOKEN}"
    response_header_timeout_seconds: 5

  - name: logging-server
    url: https://logs.example.com
    credentials:
      bearer:
        token: "${LOG_TOKEN}"

# バンドル設定
bundles:
  authz:
    service: bundle-server
    resource: /bundles/authz/bundle.tar.gz
    persist: true
    polling:
      min_delay_seconds: 30
      max_delay_seconds: 120
    signing:
      keyid: global_key
      scope: write

# Decision Log 設定
decision_logs:
  service: logging-server
  reporting:
    min_delay_seconds: 5
    max_delay_seconds: 30
  partition_name: /logs
  # 機密情報のマスキング
  mask_decision: /system/log/mask

# ステータス報告
status:
  service: bundle-server
  partition_name: /status

# キャッシュ設定
caching:
  inter_query_builtin_cache:
    max_size_bytes: 268435456  # 256MB

# サーバー設定
labels:
  app: my-service
  region: ap-northeast-1
  environment: production

# デフォルトの判断ルール
default_decision: /authz/allow
default_authorization_policy: /system/authz/allow

4.4 OPA の起動

# 基本的な起動 (サーバーモード)
opa run --server

# 設定ファイルを指定して起動
opa run --server --config-file opa-config.yaml

# ポリシーとデータファイルを指定して起動
opa run --server \
  --addr :8181 \
  --log-level info \
  --log-format json \
  policy.rego \
  data.json

# REPL モード (対話型)
opa run
> data.example.allow
true

# ワンショット評価
opa eval --data policy.rego --input input.json "data.authz.allow"

5. デプロイメントパターン

OPA はその軽量な設計により、さまざまなデプロイメントパターンに対応する。ユースケースや非機能要件に応じて最適なパターンを選択できる。

5.1 サイドカーパターン

最も一般的なパターン。各サービスの Pod 内にサイドカーとして OPA をデプロイする。

┌──────────────────────────────────────┐
│              Pod                      │
│                                      │
│  ┌──────────────┐  ┌──────────────┐ │
│  │  Application │  │  OPA         │ │
│  │  Container   │──▶│  Container   │ │
│  │              │◀──│              │ │
│  │  Port: 8080  │  │  Port: 8181  │ │
│  └──────────────┘  └──────────────┘ │
│                                      │
└──────────────────────────────────────┘

メリット:

  • レイテンシが最小 (localhost 通信)
  • 障害の影響範囲が限定的 (1 つの OPA がダウンしても他のサービスに影響しない)
  • サービスごとに異なるポリシーをロード可能

デメリット:

  • リソース消費が増加 (各 Pod に OPA のメモリ・CPU が必要)
  • ポリシーの配布先が多くなる
  • 運用管理の対象が増える

Kubernetes マニフェスト例:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-service
  template:
    metadata:
      labels:
        app: my-service
    spec:
      containers:
      - name: app
        image: my-service:latest
        ports:
        - containerPort: 8080
        env:
        - name: OPA_URL
          value: "http://localhost:8181"

      - name: opa
        image: openpolicyagent/opa:latest
        args:
        - "run"
        - "--server"
        - "--config-file=/config/opa-config.yaml"
        - "--log-level=info"
        - "--log-format=json"
        ports:
        - containerPort: 8181
        volumeMounts:
        - name: opa-config
          mountPath: /config
        resources:
          requests:
            memory: "64Mi"
            cpu: "100m"
          limits:
            memory: "256Mi"
            cpu: "500m"
        livenessProbe:
          httpGet:
            path: /health
            port: 8181
          initialDelaySeconds: 5
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /health?bundles
            port: 8181
          initialDelaySeconds: 5
          periodSeconds: 10

      volumes:
      - name: opa-config
        configMap:
          name: opa-config

5.2 デーモンパターン

各ノードに DaemonSet として OPA をデプロイし、同一ノード上の複数サービスで共有する。

┌─────────────────────────────────────────────────────┐
│                     Node                             │
│                                                      │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐          │
│  │  Pod A   │  │  Pod B   │  │  Pod C   │          │
│  │          │  │          │  │          │          │
│  └────┬─────┘  └────┬─────┘  └────┬─────┘          │
│       │              │              │                │
│       └──────────────┼──────────────┘                │
│                      │                               │
│              ┌───────▼───────┐                       │
│              │   OPA Daemon  │                       │
│              │  (DaemonSet)  │                       │
│              │  Port: 8181   │                       │
│              └───────────────┘                       │
│                                                      │
└─────────────────────────────────────────────────────┘

メリット:

  • ノードあたり 1 つの OPA で済むため、リソース効率が良い
  • ポリシーの配布先が少ない

デメリット:

  • OPA のダウンがノード上の全サービスに影響
  • ネットワークレイテンシがサイドカーより若干大きい
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: opa
spec:
  selector:
    matchLabels:
      app: opa
  template:
    metadata:
      labels:
        app: opa
    spec:
      containers:
      - name: opa
        image: openpolicyagent/opa:latest
        args:
        - "run"
        - "--server"
        - "--config-file=/config/opa-config.yaml"
        - "--addr=0.0.0.0:8181"
        ports:
        - containerPort: 8181
          hostPort: 8181
        resources:
          requests:
            memory: "256Mi"
            cpu: "250m"
          limits:
            memory: "1Gi"
            cpu: "1000m"

5.3 Go ライブラリパターン

Go 言語で記述されたアプリケーションの場合、OPA をライブラリとして組み込むことができる。ネットワーク通信が不要になるため、最高のパフォーマンスを実現する。

package main

import (
    "context"
    "encoding/json"
    "fmt"
    "log"

    "github.com/open-policy-agent/opa/rego"
)

func main() {
    ctx := context.Background()

    // Rego ポリシーを定義
    module := `
        package authz

        default allow := false

        allow if {
            input.user.role == "admin"
        }

        allow if {
            input.user.role == "editor"
            input.action == "read"
        }
    `

    // Rego インスタンスを作成
    query, err := rego.New(
        rego.Query("data.authz.allow"),
        rego.Module("policy.rego", module),
    ).PrepareForEval(ctx)
    if err != nil {
        log.Fatal(err)
    }

    // 入力データを作成
    input := map[string]interface{}{
        "user": map[string]interface{}{
            "name": "alice",
            "role": "admin",
        },
        "action": "delete",
        "resource": "/api/users/123",
    }

    // ポリシーを評価
    results, err := query.Eval(ctx, rego.EvalInput(input))
    if err != nil {
        log.Fatal(err)
    }

    if len(results) > 0 && results[0].Expressions[0].Value == true {
        fmt.Println("Access allowed")
    } else {
        fmt.Println("Access denied")
    }
}

5.4 WebAssembly (WASM) パターン

OPA ポリシーを WebAssembly にコンパイルし、ブラウザやエッジ環境で実行する。

# ポリシーを WASM にコンパイル
opa build -t wasm -e authz/allow policy.rego

# 出力される bundle.tar.gz に WASM バイナリが含まれる
tar -xzf bundle.tar.gz /policy.wasm

JavaScript からの利用例:

const { loadPolicy } = require("@open-policy-agent/opa-wasm");
const fs = require("fs");

async function evaluate() {
    const policyWasm = fs.readFileSync("policy.wasm");
    const policy = await loadPolicy(policyWasm);

    // データのセット
    policy.setData({
        roles: {
            admin: { permissions: ["read", "write", "delete"] }
        }
    });

    // 評価
    const input = {
        user: { role: "admin" },
        action: "write"
    };

    const result = policy.evaluate(input);
    console.log("Decision:", result);
}

evaluate();

5.5 デプロイメントパターンの比較

パターンレイテンシリソース効率障害分離適用先
サイドカー最小 (μs)Kubernetes マイクロサービス
デーモン小 (ms)ノード共有型環境
Go ライブラリ最小 (μs)最高最高Go アプリケーション
WASM小 (ms)ブラウザ、エッジ、非 Go 言語

6. Kubernetes との統合 (OPA Gatekeeper)

6.1 OPA Gatekeeper とは

OPA Gatekeeper は、Kubernetes の Admission Controller として動作する OPA の実装である。Kubernetes API サーバーへのリクエストをインターセプトし、ポリシーに基づいてリソースの作成・変更を許可または拒否する。

Gatekeeper は OPA の Kubernetes ネイティブな拡張であり、以下の特徴を持つ。

  • CRD ベースのポリシー管理: ConstraintTemplate と Constraint を Kubernetes のカスタムリソースとして定義
  • Audit 機能: 既存リソースに対するポリシー違反の定期的な監査
  • Mutation Webhook: リソースの自動修正 (v3.10+)
  • External Data Provider: 外部データソースとの連携

6.2 アーキテクチャ

┌───────────────────────────────────────────────────────────┐
│                  Kubernetes Cluster                        │
│                                                           │
│   ┌─────────┐    ┌──────────────┐    ┌────────────────┐  │
│   │  kubectl │───▶│ API Server   │───▶│  etcd          │  │
│   └─────────┘    └──────┬───────┘    └────────────────┘  │
│                         │                                 │
│                   Admission                               │
│                   Webhook                                 │
│                         │                                 │
│                  ┌──────▼───────┐                         │
│                  │  Gatekeeper  │                         │
│                  │  Controller  │                         │
│                  │              │                         │
│                  │ ┌──────────┐ │                         │
│                  │ │   OPA    │ │                         │
│                  │ │  Engine  │ │                         │
│                  │ └──────────┘ │                         │
│                  │ ┌──────────┐ │                         │
│                  │ │ Constraint│ │                        │
│                  │ │ Templates│ │                         │
│                  │ └──────────┘ │                         │
│                  │ ┌──────────┐ │                         │
│                  │ │  Audit   │ │                         │
│                  │ │  Engine  │ │                         │
│                  │ └──────────┘ │                         │
│                  └──────────────┘                         │
│                                                           │
└───────────────────────────────────────────────────────────┘

6.3 Gatekeeper のインストール

# Helm を使用したインストール
helm repo add gatekeeper https://open-policy-agent.github.io/gatekeeper/charts
helm install gatekeeper/gatekeeper \
  --name-template=gatekeeper \
  --namespace gatekeeper-system \
  --create-namespace \
  --set replicas=3 \
  --set audit.interval=60 \
  --set constraintViolationsLimit=100

# マニフェストを使用したインストール
kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper/v3.15.0/deploy/gatekeeper.yaml

6.4 ConstraintTemplate と Constraint

Gatekeeper では、ポリシーを ConstraintTemplate (テンプレート) と Constraint (インスタンス) の 2 層構造で定義する。

6.4.1 ConstraintTemplate の定義

ConstraintTemplate は Rego ポリシーのテンプレートを定義する。

apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8srequiredlabels
  annotations:
    description: "必須ラベルの存在を検証するテンプレート"
spec:
  crd:
    spec:
      names:
        kind: K8sRequiredLabels
      validation:
        openAPIV3Schema:
          type: object
          properties:
            labels:
              type: array
              items:
                type: string
              description: "必須ラベルのリスト"
            message:
              type: string
              description: "カスタムエラーメッセージ"
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8srequiredlabels

        violation[{"msg": msg, "details": {"missing_labels": missing}}] {
          provided := {label | input.review.object.metadata.labels[label]}
          required := {label | label := input.parameters.labels[_]}
          missing := required - provided
          count(missing) > 0
          msg := sprintf("リソース '%v' に必須ラベルが不足しています: %v", [
            input.review.object.metadata.name,
            missing
          ])
        }

6.4.2 Constraint の定義

Constraint は ConstraintTemplate のインスタンスであり、具体的なパラメータと適用範囲を定義する。

apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata:
  name: require-team-label
spec:
  enforcementAction: deny  # deny, dryrun, warn
  match:
    kinds:
    - apiGroups: [""]
      kinds: ["Namespace"]
    - apiGroups: ["apps"]
      kinds: ["Deployment", "StatefulSet"]
    excludedNamespaces:
    - kube-system
    - gatekeeper-system
  parameters:
    labels:
    - "team"
    - "environment"
    message: "チーム名と環境ラベルは必須です"

6.5 よく使用される Gatekeeper ポリシー

6.5.1 コンテナイメージの制限

apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8sallowedrepos
spec:
  crd:
    spec:
      names:
        kind: K8sAllowedRepos
      validation:
        openAPIV3Schema:
          type: object
          properties:
            repos:
              type: array
              items:
                type: string
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8sallowedrepos

        violation[{"msg": msg}] {
          container := input.review.object.spec.containers[_]
          not startswith_any(container.image, input.parameters.repos)
          msg := sprintf("コンテナ '%v' のイメージ '%v' は許可されたレジストリのものではありません。許可されたレジストリ: %v", [
            container.name, container.image, input.parameters.repos
          ])
        }

        violation[{"msg": msg}] {
          container := input.review.object.spec.initContainers[_]
          not startswith_any(container.image, input.parameters.repos)
          msg := sprintf("initContainer '%v' のイメージ '%v' は許可されたレジストリのものではありません。", [
            container.name, container.image
          ])
        }

        startswith_any(str, prefixes) {
          prefix := prefixes[_]
          startswith(str, prefix)
        }
---
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sAllowedRepos
metadata:
  name: allowed-repos
spec:
  match:
    kinds:
    - apiGroups: ["apps"]
      kinds: ["Deployment", "StatefulSet", "DaemonSet"]
    namespaces:
    - "production"
    - "staging"
  parameters:
    repos:
    - "gcr.io/my-project/"
    - "docker.io/library/"
    - "quay.io/my-org/"

6.5.2 リソース制限の必須化

apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8sresourcelimits
spec:
  crd:
    spec:
      names:
        kind: K8sResourceLimits
      validation:
        openAPIV3Schema:
          type: object
          properties:
            maxCPU:
              type: string
            maxMemory:
              type: string
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8sresourcelimits

        violation[{"msg": msg}] {
          container := input.review.object.spec.containers[_]
          not container.resources.limits
          msg := sprintf("コンテナ '%v' にリソース制限 (limits) が設定されていません", [container.name])
        }

        violation[{"msg": msg}] {
          container := input.review.object.spec.containers[_]
          not container.resources.requests
          msg := sprintf("コンテナ '%v' にリソース要求 (requests) が設定されていません", [container.name])
        }

        violation[{"msg": msg}] {
          container := input.review.object.spec.containers[_]
          container.resources.limits.cpu
          not resource_within_limit(container.resources.limits.cpu, input.parameters.maxCPU)
          msg := sprintf("コンテナ '%v'  CPU 制限 '%v' が最大値 '%v' を超えています", [
            container.name, container.resources.limits.cpu, input.parameters.maxCPU
          ])
        }

6.5.3 特権コンテナの禁止

apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8spsprivilegedcontainer
spec:
  crd:
    spec:
      names:
        kind: K8sPSPPrivilegedContainer
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8spsprivilegedcontainer

        violation[{"msg": msg}] {
          container := input.review.object.spec.containers[_]
          container.securityContext.privileged == true
          msg := sprintf("特権コンテナは許可されていません: '%v'", [container.name])
        }

        violation[{"msg": msg}] {
          container := input.review.object.spec.initContainers[_]
          container.securityContext.privileged == true
          msg := sprintf("特権 initContainer は許可されていません: '%v'", [container.name])
        }

        violation[{"msg": msg}] {
          container := input.review.object.spec.containers[_]
          container.securityContext.runAsUser == 0
          msg := sprintf("root ユーザーでの実行は許可されていません: '%v'", [container.name])
        }
---
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sPSPPrivilegedContainer
metadata:
  name: no-privileged-containers
spec:
  match:
    kinds:
    - apiGroups: ["apps"]
      kinds: ["Deployment", "StatefulSet", "DaemonSet"]
    excludedNamespaces:
    - kube-system

6.6 Audit (監査) 機能

Gatekeeper は既存リソースに対してポリシー違反を定期的にチェックする Audit 機能を提供する。

# 監査結果の確認
kubectl get k8srequiredlabels require-team-label -o yaml

# 出力例
status:
  auditTimestamp: "2025-01-15T10:30:00Z"
  totalViolations: 3
  violations:
  - enforcementAction: deny
    kind: Deployment
    name: legacy-app
    namespace: production
    message: "リソース 'legacy-app' に必須ラベルが不足しています: {\"team\"}"
  - enforcementAction: deny
    kind: Namespace
    name: test-namespace
    message: "リソース 'test-namespace' に必須ラベルが不足しています: {\"team\", \"environment\"}"
# Gatekeeper の全体的な監査ステータスを確認
kubectl describe constrainttemplate k8srequiredlabels

# すべての Constraint の違反をまとめて確認
kubectl get constraints -o json | jq '.items[] | {name: .metadata.name, violations: .status.totalViolations}'

7. マイクロサービス API 認可

7.1 HTTP API 認可パターン

OPA をマイクロサービスの API 認可に使用する基本的なパターンを示す。

┌──────────┐    ┌───────────────┐    ┌──────────────┐
│  Client  │───▶│  API Gateway  │───▶│  Backend     │
│          │    │  / Service    │    │  Service     │
└──────────┘    └───────┬───────┘    └──────────────┘
                        │
                   ┌────▼────┐
                   │   OPA   │
                   │ Sidecar │
                   └─────────┘

7.2 Go サービスでの統合例

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "strings"
)

// OPAClient は OPA との通信を行うクライアント
type OPAClient struct {
    baseURL string
    client  *http.Client
}

// OPAInput は OPA に送信する入力データ
type OPAInput struct {
    Input map[string]interface{} `json:"input"`
}

// OPAResult は OPA からの応答
type OPAResult struct {
    Result bool `json:"result"`
}

func NewOPAClient(baseURL string) *OPAClient {
    return &OPAClient{
        baseURL: baseURL,
        client:  &http.Client{},
    }
}

func (c *OPAClient) IsAllowed(input map[string]interface{}) (bool, error) {
    body, _ := json.Marshal(OPAInput{Input: input})

    resp, err := c.client.Post(
        c.baseURL+"/v1/data/httpapi/authz/allow",
        "application/json",
        bytes.NewReader(body),
    )
    if err != nil {
        return false, fmt.Errorf("OPA request failed: %w", err)
    }
    defer resp.Body.Close()

    var result OPAResult
    if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
        return false, fmt.Errorf("OPA response decode failed: %w", err)
    }

    return result.Result, nil
}

// AuthMiddleware は認可ミドルウェア
func AuthMiddleware(opa *OPAClient) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            // JWT からユーザー情報を抽出 (簡略化)
            token := r.Header.Get("Authorization")
            userInfo := extractUserInfo(token)

            input := map[string]interface{}{
                "method":   r.Method,
                "path":     strings.Split(r.URL.Path, "/"),
                "user":     userInfo,
                "headers":  flattenHeaders(r.Header),
            }

            allowed, err := opa.IsAllowed(input)
            if err != nil {
                http.Error(w, "Authorization check failed", http.StatusInternalServerError)
                return
            }
            if !allowed {
                http.Error(w, "Forbidden", http.StatusForbidden)
                return
            }

            next.ServeHTTP(w, r)
        })
    }
}

func main() {
    opa := NewOPAClient("http://localhost:8181")

    mux := http.NewServeMux()
    mux.HandleFunc("/api/v1/users", handleUsers)
    mux.HandleFunc("/api/v1/admin", handleAdmin)

    handler := AuthMiddleware(opa)(mux)
    http.ListenAndServe(":8080", handler)
}

7.3 対応する Rego ポリシー

package httpapi.authz

import input.method
import input.path
import input.user

default allow := false

# --- パブリックエンドポイント ---
allow if {
    method == "GET"
    path == ["", "api", "v1", "health"]
}

# --- 認証済みユーザー ---
allow if {
    method == "GET"
    path == ["", "api", "v1", "users"]
    user.authenticated == true
}

# --- リソースオーナー ---
allow if {
    method == "PUT"
    path == ["", "api", "v1", "users", user_id]
    user.id == user_id
}

# --- 管理者 ---
allow if {
    path[2] == "admin"
    "admin" in user.roles
}

# --- レート制限チェック ---
allow if {
    method == "POST"
    path == ["", "api", "v1", "users"]
    user.authenticated == true
    not rate_limited
}

rate_limited if {
    user.request_count_last_minute > 100
}

# --- 詳細な応答を返すバージョン ---
authz := {
    "allow": allow,
    "reason": reason,
    "headers": response_headers,
}

reason := "public endpoint" if {
    method == "GET"
    path == ["", "api", "v1", "health"]
}

reason := "insufficient permissions" if {
    not allow
}

response_headers := {"X-RateLimit-Remaining": remaining} if {
    remaining := sprintf("%d", [100 - user.request_count_last_minute])
}

7.4 Python (Flask) での統合例

import functools
import requests
from flask import Flask, request, jsonify, g

app = Flask(__name__)
OPA_URL = "http://localhost:8181/v1/data/httpapi/authz/allow"

def authorize(f):
    @functools.wraps(f)
    def decorated(*args, **kwargs):
        # リクエスト情報を OPA に送信
        opa_input = {
            "input": {
                "method": request.method,
                "path": request.path.split("/"),
                "user": {
                    "id": g.get("user_id"),
                    "roles": g.get("user_roles", []),
                    "authenticated": g.get("authenticated", False),
                },
                "query_params": dict(request.args),
                "content_type": request.content_type,
            }
        }

        try:
            resp = requests.post(OPA_URL, json=opa_input, timeout=1.0)
            result = resp.json()

            if not result.get("result", False):
                return jsonify({"error": "Forbidden"}), 403

        except requests.RequestException as e:
            app.logger.error(f"OPA authorization failed: {e}")
            # Fail-closed: OPA に到達できない場合は拒否
            return jsonify({"error": "Authorization service unavailable"}), 503

        return f(*args, **kwargs)
    return decorated

@app.route("/api/v1/users", methods=["GET"])
@authorize
def get_users():
    return jsonify({"users": [...]})

@app.route("/api/v1/admin/settings", methods=["GET", "PUT"])
@authorize
def admin_settings():
    return jsonify({"settings": {... }})

7.5 データフィルタリング

OPA はアクセス制御だけでなく、ユーザーの権限に基づいたデータフィルタリングにも使用できる。

package httpapi.filter

# ユーザーがアクセス可能なレコードのみを返す
visible_records[record] {
    some record in data.records
    record.department == input.user.department
}

visible_records[record] {
    some record in data.records
    record.classification == "public"
}

# 管理者はすべてのレコードを参照可能
visible_records[record] {
    some record in data.records
    "admin" in input.user.roles
}

# フィールドレベルのフィルタリング
sanitized_records[sanitized] {
    some record in visible_records
    sanitized := object.remove(record, masked_fields)
}

masked_fields := {"salary", "ssn"} if {
    not "hr" in input.user.roles
}

masked_fields := set() if {
    "hr" in input.user.roles
}

8. Envoy / Istio との統合

8.1 OPA-Envoy プラグイン

OPA は Envoy プロキシの External Authorization (ext_authz) フィルタと統合できる。これにより、サービスメッシュ内のすべてのトラフィックに対して統一的なポリシー適用が可能になる。

OPA には専用の Envoy プラグイン (opa-envoy-plugin) が用意されており、gRPC ベースの External Authorization API を実装している。

┌────────┐     ┌──────────────┐     ┌──────────────┐
│ Client │────▶│    Envoy     │────▶│  Upstream    │
│        │     │    Proxy     │     │  Service     │
└────────┘     └──────┬───────┘     └──────────────┘
                      │
                 ext_authz
                      │
               ┌──────▼───────┐
               │  OPA-Envoy   │
               │   Plugin     │
               └──────────────┘

8.2 OPA-Envoy の設定

OPA 設定 (opa-envoy-config.yaml)

services:
  - name: bundle-server
    url: https://bundle.example.com

bundles:
  envoy/authz:
    service: bundle-server
    resource: /bundles/envoy/authz/bundle.tar.gz

plugins:
  envoy_ext_authz_grpc:
    addr: :9191
    path: envoy/authz/allow
    dry-run: false
    enable-reflection: true
    # 判断結果のプロトバージョン
    proto-descriptor: /etc/opa/proto/auth.proto

decision_logs:
  console: true

Envoy 設定

static_resources:
  listeners:
  - name: listener_0
    address:
      socket_address:
        address: 0.0.0.0
        port_value: 10000
    filter_chains:
    - filters:
      - name: envoy.filters.network.http_connection_manager
        typed_config:
          "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
          stat_prefix: ingress_http
          route_config:
            name: local_route
            virtual_hosts:
            - name: backend
              domains: ["*"]
              routes:
              - match:
                  prefix: "/"
                route:
                  cluster: upstream_service
          http_filters:
          # External Authorization フィルタ
          - name: envoy.filters.http.ext_authz
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz
              transport_api_version: V3
              grpc_service:
                envoy_grpc:
                  cluster_name: opa
                timeout: 0.5s
              failure_mode_allow: false  # OPA 障害時はリクエストを拒否
              with_request_body:
                max_request_bytes: 8192
                allow_partial_message: true
          - name: envoy.filters.http.router
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router

  clusters:
  - name: opa
    type: STRICT_DNS
    connect_timeout: 1s
    load_assignment:
      cluster_name: opa
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address:
                address: 127.0.0.1
                port_value: 9191
    typed_extension_protocol_options:
      envoy.extensions.upstreams.http.v3.HttpProtocolOptions:
        "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions
        explicit_http_config:
          http2_protocol_options: {}

  - name: upstream_service
    type: STRICT_DNS
    connect_timeout: 1s
    load_assignment:
      cluster_name: upstream_service
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address:
                address: 127.0.0.1
                port_value: 8080

8.3 Envoy 用 Rego ポリシー

package envoy.authz

import input.attributes.request.http as http_request
import input.attributes.source.address as source_address
import input.attributes.destination.address as dest_address

default allow := false

# ヘルスチェックは常に許可
allow if {
    http_request.method == "GET"
    http_request.path == "/healthz"
}

# JWT トークンの検証
allow if {
    is_valid_token
    action_allowed
}

# トークン検証
is_valid_token if {
    token := bearer_token
    [valid, _, payload] := io.jwt.decode_verify(token, {
        "cert": data.keys.public_key,
        "iss": "https://auth.example.com",
        "aud": "my-service"
    })
    valid == true
}

# ベアラートークンの抽出
bearer_token := t if {
    v := http_request.headers.authorization
    startswith(v, "Bearer ")
    t := substring(v, count("Bearer "), -1)
}

# トークンペイロードの取得
token_payload := payload if {
    [_, payload, _] := io.jwt.decode(bearer_token)
}

# アクションの認可
action_allowed if {
    http_request.method == "GET"
    glob.match("/api/v1/users/*", ["/"], http_request.path)
}

action_allowed if {
    http_request.method == "POST"
    http_request.path == "/api/v1/users"
    "admin" in token_payload.roles
}

action_allowed if {
    http_request.method == "DELETE"
    glob.match("/api/v1/users/*", ["/"], http_request.path)
    "super_admin" in token_payload.roles
}

# サービス間通信の制御
action_allowed if {
    source_service := token_payload.sub
    dest_service := http_request.headers["x-destination-service"]
    allowed_communication[source_service][dest_service]
}

# サービス間通信マトリクス
allowed_communication := {
    "frontend": {"user-service", "product-service"},
    "user-service": {"auth-service", "notification-service"},
    "order-service": {"user-service", "product-service", "payment-service"},
}

8.4 Istio との統合

Istio サービスメッシュでは、AuthorizationPolicy を拡張して OPA を External Authorizer として使用できる。

# Istio の meshConfig に OPA を External Authorizer として登録
apiVersion: install.istio.io/v1alpha1
kind: IstioOperator
spec:
  meshConfig:
    extensionProviders:
    - name: opa-ext-authz
      envoyExtAuthzGrpc:
        service: opa.opa-system.svc.cluster.local
        port: 9191
        timeout: 1s

---
# AuthorizationPolicy で OPA を使用
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
  name: opa-authz
  namespace: production
spec:
  action: CUSTOM
  provider:
    name: opa-ext-authz
  rules:
  - to:
    - operation:
        paths: ["/api/*"]
        notPaths: ["/api/v1/health"]

9. Terraform との統合

9.1 概要

OPA は Terraform のプラン (plan) データに対してポリシーを適用し、インフラストラクチャの変更が組織のポリシーに準拠しているかを検証できる。これは Conftest ツールまたは直接 OPA を使用して実現する。

┌──────────┐    ┌──────────────┐    ┌──────────────┐    ┌──────────┐
│ terraform│───▶│ terraform    │───▶│  OPA /       │───▶│ terraform│
│  init    │    │  plan -out   │    │  Conftest    │    │  apply   │
│          │    │  tfplan      │    │  (validate)  │    │          │
└──────────┘    └──────────────┘    └──────────────┘    └──────────┘
                                          │
                                     Pass / Fail

9.2 Terraform プランの変換

# Terraform プランを作成
terraform plan -out=tfplan

# プランを JSON に変換
terraform show -json tfplan > tfplan.json

# OPA で評価
opa eval --data policy/ --input tfplan.json "data.terraform.deny"

# Conftest で評価
conftest test tfplan.json --policy policy/

9.3 Terraform ポリシーの例

9.3.1 許可されたインスタンスタイプの制限

package terraform

import input.resource_changes

# 許可されたインスタンスタイプ
allowed_instance_types := {
    "t3.micro", "t3.small", "t3.medium",
    "m5.large", "m5.xlarge",
    "c5.large", "c5.xlarge"
}

deny[msg] {
    some rc in resource_changes
    rc.type == "aws_instance"
    rc.change.after.instance_type
    not rc.change.after.instance_type in allowed_instance_types
    msg := sprintf(
        "EC2 インスタンス '%v' のインスタンスタイプ '%v' は許可されていません。許可タイプ: %v",
        [rc.name, rc.change.after.instance_type, allowed_instance_types]
    )
}

9.3.2 セキュリティグループの検証

package terraform

deny[msg] {
    some rc in resource_changes
    rc.type == "aws_security_group_rule"
    rc.change.after.type == "ingress"
    rc.change.after.cidr_blocks[_] == "0.0.0.0/0"
    rc.change.after.from_port <= 22
    rc.change.after.to_port >= 22
    msg := sprintf(
        "セキュリティグループルール '%v': SSH (ポート 22) を全世界に公開することは禁止されています",
        [rc.name]
    )
}

deny[msg] {
    some rc in resource_changes
    rc.type == "aws_security_group_rule"
    rc.change.after.type == "ingress"
    rc.change.after.cidr_blocks[_] == "0.0.0.0/0"
    rc.change.after.from_port <= 3389
    rc.change.after.to_port >= 3389
    msg := sprintf(
        "セキュリティグループルール '%v': RDP (ポート 3389) を全世界に公開することは禁止されています",
        [rc.name]
    )
}

9.3.3 S3 バケットの暗号化必須化

package terraform

deny[msg] {
    some rc in resource_changes
    rc.type == "aws_s3_bucket"
    rc.change.actions[_] == "create"
    not has_encryption(rc)
    msg := sprintf(
        "S3 バケット '%v': サーバーサイド暗号化が設定されていません。AES256 または aws:kms を使用してください",
        [rc.name]
    )
}

has_encryption(rc) if {
    rc.change.after.server_side_encryption_configuration[_].rule[_].apply_server_side_encryption_by_default[_].sse_algorithm
}

deny[msg] {
    some rc in resource_changes
    rc.type == "aws_s3_bucket"
    rc.change.actions[_] == "create"
    not has_versioning(rc)
    msg := sprintf(
        "S3 バケット '%v': バージョニングが有効になっていません",
        [rc.name]
    )
}

has_versioning(rc) if {
    rc.change.after.versioning[_].enabled == true
}

deny[msg] {
    some rc in resource_changes
    rc.type == "aws_s3_bucket"
    rc.change.actions[_] == "create"
    not has_public_access_block(rc)
    msg := sprintf(
        "S3 バケット '%v': パブリックアクセスブロックを設定してください",
        [rc.name]
    )
}

9.3.4 タグの必須化

package terraform

required_tags := {"Environment", "Team", "CostCenter", "ManagedBy"}

deny[msg] {
    some rc in resource_changes
    rc.change.actions[_] == "create"
    taggable_resource(rc.type)
    provided_tags := {tag | rc.change.after.tags[tag]}
    missing := required_tags - provided_tags
    count(missing) > 0
    msg := sprintf(
        "リソース '%v' (%v) に必須タグが不足しています: %v",
        [rc.name, rc.type, missing]
    )
}

taggable_resource(type) if {
    taggable := {
        "aws_instance", "aws_s3_bucket", "aws_rds_instance",
        "aws_lambda_function", "aws_ecs_service", "aws_eks_cluster",
        "aws_vpc", "aws_subnet", "aws_security_group"
    }
    type in taggable
}

9.4 Conftest の活用

Conftest は OPA をベースとした構成ファイルのテストツールであり、Terraform 以外にも Kubernetes マニフェスト、Dockerfile、各種設定ファイルに対してポリシーテストを実行できる。

# インストール
brew install conftest

# Terraform プランのテスト
conftest test tfplan.json --policy policy/ --output table

# Kubernetes マニフェストのテスト
conftest test k8s/ --policy policy/k8s/ --all-namespaces

# Dockerfile のテスト
conftest test Dockerfile --policy policy/docker/

# 複数フォーマットのサポート
conftest test --policy policy/ \
  --input json tfplan.json \
  --input yaml k8s-manifests/ \
  --input dockerfile Dockerfile

Dockerfile ポリシーの例

package docker

deny[msg] {
    input.Cmd == "from"
    val := input.Value[0]
    contains(val, ":latest")
    msg := "latest タグの使用は禁止されています。具体的なバージョンタグを指定してください"
}

deny[msg] {
    input.Cmd == "user"
    input.Value[0] == "root"
    msg := "root ユーザーでの実行は禁止されています"
}

deny[msg] {
    input.Cmd == "run"
    some val in input.Value
    contains(val, "curl")
    contains(val, "|")
    contains(val, "sh")
    msg := "curl | sh パターンは禁止されています。セキュリティリスクがあります"
}

10. ポリシーテスト

10.1 テストの重要性

ポリシーはコードと同様にバグが入り込む可能性がある。特にセキュリティポリシーのバグは致命的な結果をもたらす。OPA は組み込みのテストフレームワークを提供し、ポリシーの品質を保証する。

10.2 テストの基本

テストファイルは _test.rego のサフィックスを持ち、test_ プレフィックスのルールとして定義する。

# policy.rego
package authz

default allow := false

allow if {
    input.user.role == "admin"
}

allow if {
    input.user.role == "editor"
    input.method == "GET"
}

allow if {
    input.user.role == "editor"
    input.method == "PUT"
    input.user.id == input.resource.owner_id
}
# policy_test.rego
package authz

# --- 管理者のテスト ---
test_admin_allow_read if {
    allow with input as {
        "user": {"role": "admin", "id": "user1"},
        "method": "GET",
        "resource": {"owner_id": "user2"}
    }
}

test_admin_allow_delete if {
    allow with input as {
        "user": {"role": "admin", "id": "user1"},
        "method": "DELETE",
        "resource": {"owner_id": "user2"}
    }
}

# --- 編集者のテスト ---
test_editor_allow_read if {
    allow with input as {
        "user": {"role": "editor", "id": "user1"},
        "method": "GET",
        "resource": {"owner_id": "user2"}
    }
}

test_editor_deny_delete if {
    not allow with input as {
        "user": {"role": "editor", "id": "user1"},
        "method": "DELETE",
        "resource": {"owner_id": "user2"}
    }
}

test_editor_allow_put_own_resource if {
    allow with input as {
        "user": {"role": "editor", "id": "user1"},
        "method": "PUT",
        "resource": {"owner_id": "user1"}
    }
}

test_editor_deny_put_other_resource if {
    not allow with input as {
        "user": {"role": "editor", "id": "user1"},
        "method": "PUT",
        "resource": {"owner_id": "user2"}
    }
}

# --- 閲覧者のテスト ---
test_viewer_deny_all if {
    not allow with input as {
        "user": {"role": "viewer", "id": "user1"},
        "method": "GET",
        "resource": {"owner_id": "user1"}
    }
}

# --- 未認証ユーザーのテスト ---
test_unauthenticated_deny if {
    not allow with input as {
        "method": "GET",
        "resource": {"owner_id": "user1"}
    }
}

10.3 テストの実行

# テストの実行
opa test . -v

# 出力例:
# policy_test.rego:
#   data.authz.test_admin_allow_read: PASS (1.234ms)
#   data.authz.test_admin_allow_delete: PASS (0.987ms)
#   data.authz.test_editor_allow_read: PASS (0.876ms)
#   data.authz.test_editor_deny_delete: PASS (0.654ms)
#   data.authz.test_editor_allow_put_own_resource: PASS (0.543ms)
#   data.authz.test_editor_deny_put_other_resource: PASS (0.432ms)
#   data.authz.test_viewer_deny_all: PASS (0.321ms)
#   data.authz.test_unauthenticated_deny: PASS (0.210ms)
# -----------------------------------------------
# PASS: 8/8

# カバレッジの計測
opa test . --coverage --format=json

# 出力例:
# {
#   "files": {
#     "policy.rego": {
#       "covered": [3, 5, 6, 9, 10, 13, 14, 15],
#       "not_covered": [],
#       "coverage": 100
#     }
#   }
# }

# 特定のテストのみ実行
opa test . -v --run "test_admin"

# ベンチマーク
opa test . --bench --count 100

10.4 with キーワードによるモック

with キーワードを使用して、テスト時にデータや組み込み関数をモックできる。

# テスト対象のポリシー
package authz

allow if {
    is_business_hours
    user_has_permission
}

is_business_hours if {
    now := time.now_ns()
    [hour, _, _] := time.clock([now, "Asia/Tokyo"])
    hour >= 9
    hour < 18
}

user_has_permission if {
    some role in data.user_roles[input.user]
    some perm in data.roles[role].permissions
    perm == input.action
}
# テスト
package authz

# 営業時間内のテスト (time.now_ns をモック)
test_allow_during_business_hours if {
    # 2025-01-15 12:00:00 JST のナノ秒
    allow with input as {
        "user": "alice",
        "action": "read"
    }
    with data.user_roles as {"alice": ["admin"]}
    with data.roles as {"admin": {"permissions": ["read", "write"]}}
    with time.now_ns as 1736910000000000000  # JST 12:00
}

# 営業時間外のテスト
test_deny_outside_business_hours if {
    not allow with input as {
        "user": "alice",
        "action": "read"
    }
    with data.user_roles as {"alice": ["admin"]}
    with data.roles as {"admin": {"permissions": ["read", "write"]}}
    with time.now_ns as 1736946000000000000  # JST 22:00
}

# HTTP 呼び出しのモック
test_external_api_check if {
    allow with input as {"user": "alice"}
    with http.send as {"status_code": 200, "body": {"valid": true}}
}

10.5 CI/CD パイプラインでのテスト

# GitHub Actions の例
name: OPA Policy Test

on:
  pull_request:
    paths:
    - 'policies/**'

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4

    - name: Setup OPA
      uses: open-policy-agent/setup-opa@v2
      with:
        version: latest

    - name: Run OPA Tests
      run: |
        opa test policies/ -v --coverage --format=json > coverage.json
        echo "Coverage Report:"
        cat coverage.json | jq '.files | to_entries[] | "\(.key): \(.value.coverage)%"'

    - name: Check Coverage Threshold
      run: |
        COVERAGE=$(cat coverage.json | jq '[.files[].coverage] | add / length')
        echo "Average coverage: ${COVERAGE}%"
        if (( $(echo "$COVERAGE < 80" | bc -l) )); then
          echo "Coverage ${COVERAGE}% is below threshold of 80%"
          exit 1
        fi

    - name: OPA Check (Format & Type Check)
      run: |
        opa fmt --diff --fail policies/
        opa check policies/ --strict

11. バンドルとポリシー配布

11.1 バンドルの概要

OPA のバンドル機能は、ポリシーとデータを一つのパッケージとしてまとめ、リモートサーバーから OPA エージェントに配布する仕組みである。大規模な環境でのポリシー管理に不可欠な機能であり、以下の利点がある。

  • ポリシーのバージョン管理と配布の自動化
  • ポリシーの署名による改ざん検知
  • 差分更新による効率的な配布

11.2 バンドルの構造

bundle.tar.gz
├── .manifest                    # バンドルのメタデータ
├── .signatures.json             # ポリシーの署名 (オプション)
├── authz/
│   ├── policy.rego              # Rego ポリシーファイル
│   ├── policy_test.rego         # テストファイル (通常は除外)
│   └── data.json                # パッケージスコープのデータ
├── rbac/
│   ├── roles.rego
│   └── data.json
└── data.json                    # ルートスコープのデータ

11.3 バンドルのビルド

# 基本的なバンドルのビルド
opa build -b policies/ -o bundle.tar.gz

# エントリポイントを指定してバンドルをビルド
opa build -b policies/ \
  -e authz/allow \
  -e rbac/user_permissions \
  -o bundle.tar.gz

# WASM を含むバンドルのビルド
opa build -b policies/ \
  -t wasm \
  -e authz/allow \
  -o bundle.tar.gz

# テストファイルを除外
opa build -b policies/ \
  --ignore "*_test.rego" \
  -o bundle.tar.gz

# リビジョンを指定
opa build -b policies/ \
  --revision "v1.2.3-abc123" \
  -o bundle.tar.gz

11.4 バンドルの署名

バンドルの署名機能により、配布されるポリシーの改ざんを検知できる。

# 署名鍵の生成
openssl genrsa -out private_key.pem 2048
openssl rsa -in private_key.pem -pubout -out public_key.pem

# 署名付きバンドルのビルド
opa build -b policies/ \
  --signing-key private_key.pem \
  --signing-alg RS256 \
  --claims-file claims.json \
  -o bundle.tar.gz
// claims.json
{
  "iss": "policy-team@example.com",
  "scope": "write",
  "keyid": "production-key-1",
  "exp": 1735689600
}

OPA 側の検証設定:

# opa-config.yaml
bundles:
  authz:
    service: bundle-server
    resource: /bundles/authz/bundle.tar.gz
    signing:
      keyid: production-key-1
      scope: write

keys:
  production-key-1:
    algorithm: RS256
    key: |
      -----BEGIN PUBLIC KEY-----
      MIIBIjANBgkqhk...
      -----END PUBLIC KEY-----

11.5 バンドル配布サーバー

バンドル配布サーバーは HTTP GET リクエストに対してバンドルを返すシンプルな HTTP サーバーである。以下のオプションが利用可能。

11.5.1 Nginx ベースの静的配布

server {
    listen 8888;
    server_name bundle.example.com;

    location /bundles/ {
        root /var/www/bundles;
        autoindex off;

        # ETag サポート (変更検知)
        etag on;

        # キャッシュ制御
        add_header Cache-Control "no-cache, must-revalidate";

        # アクセス制御
        auth_request /auth;
    }

    location = /auth {
        internal;
        proxy_pass http://auth-service:8080/verify;
        proxy_pass_request_body off;
        proxy_set_header Content-Length "";
    }
}

11.5.2 OCI レジストリを使用した配布

OPA はバンドルを OCI (Open Container Initiative) アーティファクトとしてコンテナレジストリに保存・取得できる。

# バンドルを OCI レジストリにプッシュ
opa build -b policies/ -o bundle.tar.gz
oras push ghcr.io/myorg/opa-bundles/authz:v1.0.0 \
  --artifact-type application/vnd.oci.image.layer.v1.tar+gzip \
  bundle.tar.gz

# OPA の設定
# opa-config.yaml
services:
  - name: ghcr
    url: https://ghcr.io
    type: oci
    credentials:
      bearer:
        token: "${GITHUB_TOKEN}"

bundles:
  authz:
    service: ghcr
    resource: myorg/opa-bundles/authz:v1.0.0
    persist: true
    polling:
      min_delay_seconds: 60
      max_delay_seconds: 300

11.5.3 S3 ベースの配布

services:
  - name: s3
    url: https://my-bucket.s3.ap-northeast-1.amazonaws.com
    credentials:
      s3_signing:
        environment_credentials: {}
        # または明示的な認証情報
        # access_key: "${AWS_ACCESS_KEY_ID}"
        # secret_key: "${AWS_SECRET_ACCESS_KEY}"
        # token: "${AWS_SESSION_TOKEN}"

bundles:
  authz:
    service: s3
    resource: /bundles/authz/bundle.tar.gz
    persist: true
    polling:
      min_delay_seconds: 30
      max_delay_seconds: 120

11.6 ステータス報告

OPA は自身の状態 (バンドルのロード状況、エラーなど) を外部に報告できる。

status:
  service: management-server
  partition_name: /status

# ステータス API のレスポンス例
# GET /status
# {
#   "labels": {
#     "app": "my-service",
#     "region": "ap-northeast-1"
#   },
#   "bundles": {
#     "authz": {
#       "name": "authz",
#       "active_revision": "v1.2.3-abc123",
#       "last_successful_download": "2025-01-15T10:30:00Z",
#       "last_successful_activation": "2025-01-15T10:30:01Z"
#     }
#   },
#   "plugins": {
#     "bundle": {"state": "OK"},
#     "decision_logs": {"state": "OK"},
#     "status": {"state": "OK"}
#   }
# }

12. パフォーマンスチューニング

12.1 パフォーマンス特性

OPA はインメモリで動作するため、基本的に非常に高速である。しかし、ポリシーの複雑さやデータ量によってはパフォーマンスが問題になることがある。

典型的なパフォーマンス指標:

シナリオ平均レイテンシ99 パーセンタイル
シンプルな RBAC チェック0.05ms0.1ms
JWT 検証 + RBAC0.5ms1.0ms
複雑な ABAC (50+ ルール)1.0ms3.0ms
大規模データセット (100K レコード)5.0ms15.0ms
外部 HTTP 呼び出し含む50ms+200ms+

12.2 プロファイリング

# プロファイリングの有効化
opa eval --data policy.rego --input input.json \
  --profile --format=pretty \
  "data.authz.allow"

# 出力例:
# +----------+----------+----------+---------+
# |   TIMER  |   EVAL   |   REDO   |  TOTAL  |
# +----------+----------+----------+---------+
# | query    | 125.3µs  | 45.2µs   | 170.5µs |
# | authz    |  98.1µs  | 32.1µs   | 130.2µs |
# | rbac     |  45.6µs  | 12.3µs   |  57.9µs |
# +----------+----------+----------+---------+

# ベンチマーク
opa bench --data policy.rego --input input.json \
  "data.authz.allow"

# 出力例:
# +-----------------------------+--------+
# |          BENCHMARK          |  TIME  |
# +-----------------------------+--------+
# | data.authz.allow            | 125µs  |
# | iterations: 10000           |        |
# | allocs: 45                  |        |
# | heap: 12.3KB                |        |
# +-----------------------------+--------+

12.3 最適化テクニック

12.3.1 部分評価 (Partial Evaluation) の活用

部分評価は、入力の一部が既知の場合に、残りの入力に依存する最小限のポリシーを事前に計算する手法である。

# 部分評価 API
curl -X POST http://localhost:8181/v1/compile \
  -H "Content-Type: application/json" \
  -d '{
    "query": "data.authz.allow == true",
    "input": {
      "user": {"role": "admin"}
    },
    "unknowns": ["input.action", "input.resource"]
  }'

# 結果: role=admin に対して最適化されたポリシーが返される

12.3.2 インデックスの活用

OPA は特定のパターンでルールを記述すると、自動的にインデックスを構築してパフォーマンスを向上させる。

# ✅ インデックスが効くパターン
# 完全一致 (Equality)
allow if {
    data.roles[input.user.role]  # role でインデックス参照
}

# ✅ セットメンバーシップ
allow if {
    input.action in data.allowed_actions  # セットのルックアップ
}

# ❌ インデックスが効かないパターン (避けるべき)
# 線形走査
allow if {
    some role in data.all_roles
    role.name == input.user.role  # 全件走査が発生
}

12.3.3 http.send のキャッシュ

# キャッシュを有効にした http.send
response := http.send({
    "method": "GET",
    "url": "https://api.example.com/users",
    "cache": true,
    "force_cache": true,
    "force_cache_duration_seconds": 300  # 5 分間キャッシュ
})

OPA の設定でキャッシュサイズを調整:

caching:
  inter_query_builtin_cache:
    max_size_bytes: 536870912  # 512MB
    forced_eviction_threshold_percentage: 80
    stale_entry_eviction_period_seconds: 60

12.3.4 データ構造の最適化

# ❌ 非効率: 線形走査
deny if {
    some user in data.blocked_users  # 配列の走査
    user == input.user.id
}

# ✅ 効率的: セットのルックアップ
deny if {
    data.blocked_users_set[input.user.id]  # O(1) ルックアップ
}

# データの変換
# blocked_users: ["user1", "user2", "user3"]  → 非効率
# blocked_users_set: {"user1": true, "user2": true, "user3": true}  → 効率的

12.4 リソース設定のガイドライン

環境CPUメモリバンドルサイズ
小規模 (< 100 ルール)100m64Mi< 1MB
中規模 (100-500 ルール)250m256Mi1-10MB
大規模 (500+ ルール)500m512Mi-1Gi10-50MB
データヘビー (100K+ レコード)1000m2Gi+50MB+

12.5 メトリクスとモニタリング

OPA は Prometheus 形式のメトリクスを公開する。

# Prometheus メトリクスの取得
curl http://localhost:8181/metrics

# 主要なメトリクス
# opa_decision_logs_total - 判断ログの総数
# opa_bundle_loaded_total - バンドルロードの総数
# http_request_duration_seconds - HTTP リクエストのレイテンシ
# opa_rego_query_eval_ns - Rego クエリ評価のナノ秒

Grafana ダッシュボードの設定例:

{
  "panels": [
    {
      "title": "OPA Query Latency (p99)",
      "type": "graph",
      "targets": [
        {
          "expr": "histogram_quantile(0.99, rate(http_request_duration_seconds_bucket{handler=\"/v1/data\"}[5m]))",
          "legendFormat": "p99"
        }
      ]
    },
    {
      "title": "OPA Decision Rate",
      "type": "stat",
      "targets": [
        {
          "expr": "rate(http_request_duration_seconds_count{handler=\"/v1/data\"}[5m])"
        }
      ]
    },
    {
      "title": "Bundle Load Status",
      "type": "table",
      "targets": [
        {
          "expr": "opa_bundle_loaded_total"
        }
      ]
    }
  ]
}

13. セキュリティ考慮事項

13.1 OPA 自体のセキュリティ

OPA をプロダクション環境で運用する際には、OPA 自体のセキュリティを確保する必要がある。

13.1.1 認証と認可

OPA の管理 API には認証・認可を設定すべきである。

# opa-config.yaml

# TLS の設定
addr: ":8181"
tls:
  cert_file: /certs/server.crt
  private_key_file: /certs/server.key
  ca_cert_file: /certs/ca.crt
  # クライアント証明書の要求
  client_auth_type: require_and_verify_client_cert

# 認可ポリシー (OPA 自身の API に対するアクセス制御)
default_authorization_policy: /system/authz/allow
# system/authz.rego
package system.authz

default allow := false

# ヘルスチェックは誰でもアクセス可能
allow if {
    input.path == ["health"]
}

# メトリクスは監視システムからのみ
allow if {
    input.path == ["metrics"]
    net.cidr_contains("10.0.0.0/8", input.identity)
}

# ポリシー評価は認証済みクライアントのみ
allow if {
    input.path[0] == "v1"
    input.path[1] == "data"
    input.identity != ""
}

# ポリシー管理は管理者のみ
allow if {
    input.path[0] == "v1"
    input.path[1] == "policies"
    input.identity == "admin-client"
}

13.1.2 Decision Log のマスキング

判断ログに機密情報が含まれる場合、マスキングポリシーを設定する。

# system/log.rego
package system.log

# 機密フィールドのマスキング
mask["/input/request/headers/authorization"] {
    input.input.request.headers.authorization
}

mask["/input/user/password"] {
    input.input.user.password
}

mask["/input/token"] {
    input.input.token
}

# 特定のパスの判断ログを完全に除外
mask["/input"] {
    input.path == "internal/sensitive"
}

13.1.3 ネットワークセキュリティ

# Kubernetes NetworkPolicy
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: opa-network-policy
  namespace: opa-system
spec:
  podSelector:
    matchLabels:
      app: opa
  policyTypes:
  - Ingress
  - Egress
  ingress:
  # 同一 Pod のアプリケーションコンテナからのみアクセス許可
  - from:
    - podSelector:
        matchLabels:
          opa-sidecar: "true"
    ports:
    - port: 8181
      protocol: TCP
  egress:
  # バンドルサーバーへのアクセス
  - to:
    - namespaceSelector:
        matchLabels:
          name: bundle-system
    ports:
    - port: 443
      protocol: TCP
  # DNS
  - to:
    - namespaceSelector: {}
    ports:
    - port: 53
      protocol: UDP
    - port: 53
      protocol: TCP

13.2 ポリシー記述のセキュリティ

13.2.1 Fail-Closed 設計

# ✅ 正しい: デフォルトは拒否
package authz

default allow := false

allow if {
    # 明示的に許可する条件
}

# ❌ 危険: デフォルトが許可
# default allow := true
#
# deny if {
#     # 明示的に拒否する条件
# }
# (deny リストに漏れがあるとすべて許可されてしまう)

13.2.2 入力の検証

package authz

default allow := false

# 入力の構造を検証
allow if {
    # 必須フィールドの存在を確認
    input.user
    input.user.id
    input.user.roles
    is_array(input.user.roles)

    # 実際の認可ロジック
    "admin" in input.user.roles
}

13.2.3 外部呼び出しの制限

# ❌ 危険: 無制限の外部 HTTP 呼び出し
response := http.send({
    "method": "GET",
    "url": input.callback_url  # ユーザーが任意の URL を指定できてしまう (SSRF)
})

# ✅ 安全: ホワイトリストによる制限
response := http.send({
    "method": "GET",
    "url": url
}) if {
    url := concat("", ["https://api.internal.example.com/users/", input.user_id])
    regex.match(`^[a-zA-Z0-9-]+$`, input.user_id)  # 入力のバリデーション
}

14. エコシステムとツール

14.1 主要なツールとプロジェクト

14.1.1 OPA Gatekeeper

前述のとおり、Kubernetes Admission Controller として OPA を運用するためのプロジェクト。CRD ベースのポリシー管理と Audit 機能を提供する。

14.1.2 Conftest

構造化された設定ファイル (Kubernetes YAML, Terraform HCL, Dockerfile, JSON, XML 等) に対してポリシーテストを実行するツール。CI/CD パイプラインでの利用に最適化されている。

# インストール
brew install conftest

# Kubernetes マニフェストのテスト
conftest test deployment.yaml -p policy/ --output table

# 複数のファイルフォーマットに対応
conftest test --policy policy/ \
  deployment.yaml \        # Kubernetes YAML
  terraform.tfplan.json \  # Terraform Plan
  Dockerfile               # Dockerfile

14.1.3 Styra DAS (Declarative Authorization Service)

Styra 社が提供する OPA の商用管理プラットフォーム。以下の機能を提供する。

  • Web UI によるポリシーの作成・編集
  • バンドルの自動配布
  • Decision Log の可視化と分析
  • ポリシーの影響分析 (Impact Analysis)
  • コンプライアンスレポーティング

14.1.4 OPA Playground

ブラウザ上で Rego ポリシーを実行・テストできるオンラインツール。学習やプロトタイピングに便利。

14.1.5 Regal — Rego Linter

Rego コードの品質を保つための Linter ツール。

# インストール
brew install styrainc/packages/regal

# Lint の実行
regal lint policies/

# 出力例:
# Rule:         opa-fmt
# Description:  File should be formatted with `opa fmt`
# Category:     style
# Location:     policy.rego:15
#
# Rule:         use-assignment-operator
# Description:  Use := for assignment, not =
# Category:     style
# Location:     policy.rego:22

# 設定ファイル (.regal/config.yaml)
# rules:
#   style:
#     opa-fmt:
#       level: error
#     line-length:
#       level: warning
#       max-line-length: 120

14.2 ライブラリとフレームワーク

言語ライブラリ用途
Gogithub.com/open-policy-agent/opa/regoOPA を Go ライブラリとして組み込み
Pythonopa-python-clientOPA REST API クライアント
Javaopa-javaOPA REST API クライアント
Node.js@open-policy-agent/opa-wasmWASM ベースのポリシー評価
Rustopa-wasmRust からの WASM ポリシー評価
.NETOPA.NET.NET 用 OPA クライアント

14.3 統合パターン一覧

統合先ツール/方法主な用途
KubernetesOPA GatekeeperAdmission Control
Envoy / IstioOPA-Envoy Pluginサービスメッシュ認可
TerraformConftest / OPAインフラポリシー
KafkaOPA REST APIトピックアクセス制御
PostgreSQLOPA REST API行レベルセキュリティ
CI/CDConftestコードレビュー、ゲート
API Gateway (Kong)Kong OPA PluginAPI 認可
NGINXnginx-opa-pluginリバースプロキシ認可
SSHPAM OPA moduleサーバーアクセス制御
DockerConftestイメージポリシー

15. まとめとベストプラクティス

15.1 ベストプラクティス一覧

ポリシー設計

#ベストプラクティス説明
1Fail-Closed 設計default allow := false を必ず設定し、明示的に許可されたケースのみアクセスを許可する
2パッケージの適切な分割機能ごとにパッケージを分割し、再利用性と可読性を高める
3ポリシーとデータの分離ハードコーディングされた値を避け、データとして外部化する
4テストの徹底すべてのルールに対してユニットテストを記述し、カバレッジ 80% 以上を目指す
5Rego のフォーマットopa fmt を CI/CD で強制し、一貫したコードスタイルを維持する
6Regal Linter の使用コード品質を自動的にチェックし、一般的な間違いを防止する

運用

#ベストプラクティス説明
7バンドルの署名本番環境ではバンドルの署名を有効にし、改ざんを防止する
8Decision Log の有効化すべてのポリシー判断をログに記録し、監査証跡を確保する
9ヘルスチェックの設定/health?bundles を使用し、バンドルがロードされた状態でのみトラフィックを受け入れる
10段階的なロールアウトdryrun / warn モードで新しいポリシーをテストしてから deny に切り替える
11リソース制限の設定CPU・メモリの requests/limits を適切に設定する
12メトリクスの監視Prometheus メトリクスを収集し、レイテンシとエラー率を監視する

パフォーマンス

#ベストプラクティス説明
13インデックスの活用セットのメンバーシップチェックやオブジェクトのキーアクセスを活用し、線形走査を避ける
14http.send の最小化外部 HTTP 呼び出しを最小限にし、必要な場合はキャッシュを有効にする
15データ構造の最適化頻繁に検索されるデータはオブジェクト (マップ) として構造化する
16部分評価の活用入力の一部が既知の場合、部分評価を使用してパフォーマンスを向上させる

15.2 アンチパターン

#アンチパターン問題推奨
1default allow := trueすべてのリクエストがデフォルトで許可されるdefault allow := false を使用
2ポリシー内でのハードコーディングポリシーの変更にコード変更が必要データとして外部化する
3テストなしのデプロイポリシーのバグが本番に到達するCI/CD でテストを強制する
4過度な http.send の使用レイテンシの増加、外部依存の増加データをバンドルに含めるか、キャッシュを使用する
5巨大な単一ポリシーファイル可読性・保守性の低下パッケージに分割する
6Decision Log のマスキング忘れ機密情報がログに記録されるマスキングポリシーを設定する

15.3 導入ロードマップ

フェーズ 1: 評価と PoC (1-2 週間)

  • OPA のインストールとローカル環境での試用
  • 既存の認可ロジックの洗い出し
  • 代表的なユースケースの Rego ポリシー化
  • パフォーマンスのベンチマーク

フェーズ 2: 開発環境での導入 (2-4 週間)

  • サイドカーパターンでの OPA デプロイ
  • バンドル配布インフラの構築
  • CI/CD パイプラインでのポリシーテストの導入
  • 開発チームへのトレーニング

フェーズ 3: ステージング環境での検証 (2-4 週間)

  • dryrun モードでのポリシー適用
  • Decision Log の分析と偽陽性/偽陰性の確認
  • パフォーマンスの検証とチューニング
  • 監視ダッシュボードの整備

フェーズ 4: 本番環境での展開 (2-4 週間)

  • 段階的なトラフィック切り替え (カナリアリリース)
  • deny モードへの切り替え
  • アラートの設定
  • 運用ドキュメントの整備

フェーズ 5: 拡張と最適化 (継続的)

  • 新しいユースケースへの適用拡大
  • ポリシーの継続的な改善
  • Gatekeeper による Kubernetes ポリシーの追加
  • Conftest による CI/CD ポリシーゲートの追加

15.4 OPA の将来展望

OPA は活発に開発が続けられており、以下の領域で進化が期待される。

  • Rego v1: Rego 言語の改良 (将来の破壊的変更なし)
  • WASM の成熟: ブラウザ・エッジでのポリシー評価の普及
  • IR (Intermediate Representation): より効率的なポリシー評価エンジン
  • External Data Provider: Gatekeeper の外部データ連携の強化
  • Policy Composition: 複数のポリシーの組み合わせ機能の改善
  • Cloud Native 統合の深化: 新しいクラウドサービスやプラットフォームとの統合

15.5 結論

Open Policy Agent は、クラウドネイティブ時代における「Policy as Code」の標準的なソリューションとして確固たる地位を築いている。汎用ポリシーエンジンとしての柔軟性、Rego 言語の表現力、そして CNCF 卒業プロジェクトとしてのエコシステムの広がりは、OPA を組織のポリシー管理基盤として採用するための強力な根拠となる。

重要なのは、OPA はあくまで「判断エンジン」であり、施行は各システムが行うという設計哲学である。この分離により、OPA は特定のドメインに縛られることなく、Kubernetes、マイクロサービス、CI/CD、インフラストラクチャなど、あらゆるレイヤーで一貫したポリシー管理を実現する。

本記事で紹介した設定例やパターンを参考に、段階的に OPA を導入し、組織全体のセキュリティとコンプライアンスを向上させていただきたい。


参考リソース: