🧩

Cerbos/OPAでMCPサーバに“超”細粒度RBAC/ABACを入れる

に公開

1. はじめに

MCP(Model Context Protocol)は、AIクライアント(Claude/ChatGPT/IDEなど)と外部のデータ・ツールを標準化された手順でつなぐオープンプロトコルです。サーバ側は「ツール」を公開し、クライアントやその背後のLLMが自動的に呼び出せる設計です(人間の承認UI=human-in-the-loopを挟む運用が推奨)。つまり “実行”という強い権限がMCPサーバ側に集まります。ここに細粒度の許可制御(RBAC/ABAC)が無いと、意図しない実行・過剰権限のリスクが生まれます。(MCP Tools; Security Best Practices; Specification – Implementation Guidelines)

MCPの初期化時にはクライアント情報(name/version) がやり取りされます(clientInfo)。これは申告値であり真正性はプロトコル外のため、社内・閉域利用であってもmTLS/ネットワーク境界/署名付きヘッダなどで補強する前提で、属性(どのクライアントから来たのか)として取り込みます。これをユーザID/ロール/ツール名/引数と合わせてポリシー判定に使うのが本記事の基本設計です。(Lifecycle; Authorization; Security Best Practices)

この記事のゴール:MCPサーバに対し、誰が/どのクライアントから/どのツールを/どんな引数で実行してよいかをCerbosまたはOPAで判定し、ツール呼び出しの直前でブロックできる最小構成を作る。

:記事中のコードは最小限に留め、手順・全文コードはGitHubに集約します(下記「🔗 リポジトリ」参照)。


1.1. 本記事で作るもの(設計全体図)

以下で 「誰が/どのクライアントから/どのツールを/どんな引数で」をポリシー化 します。

  • MCPサーバ(Python/FastMCP)
    ツール呼び出しの直前にPDP(CerbosまたはOPA)へ**/check**を実行し、許可なら実行/拒否ならエラーを返す。
  • PDP(Policy Decision Point)
    • Cerbos版/api/check/resourcesprincipal/resource/actionを送って許可判定(条件はCELでABAC、Outputsで拒否理由等を返却可)。(Cerbos API; Policy Outputs)
    • OPA版Regoでポリシーを記述し、/v1/data/...Data APIで判定(ブールに限らず構造化レスポンスも返せる)。(OPA REST API; OPA Intro)

シンプルに説明すると
「(※事前に tools/list で発見)ツール呼び出しを受けたら、まず『誰/クライアント/ツール/引数』を抽出 → PDPに照会 → Denyなら実行せずログ&通知」(MCP Tools)


1.2. まずは用語の確認(MCP/Cerbos/OPAの超概要)

1.2.1 MCPとは

  • MCPとは? AIと外部システムをつなぐ標準プロトコルツールリソースプロンプトをサーバが公開し、クライアントから呼び出す。(Model Context Protocol)
  • なにを守る? 呼び出せるToolをコントロールしたい。名前・スキーマを持ち、LLMが自動判断で実行できるが、人間の承認フロー(human-in-the-loop) 導入が推奨。ここに許可制御をかける。 (Model Context Protocol; Security Best Practices; Specification – Implementation Guidelines)
  • クライアント識別は? 初期化でclientInfo.name/versionが来る(実装例・仕様記述あり)。ABACの属性として利用できるが、申告値であり真正性はプロトコル外→mTLS/署名ヘッダ等の補強を前提に扱う。(Model Context Protocol; Lifecycle; Authorization)

注意:clientInfo自体は “宣言値”であり、真正性の保証はプロトコル外。社内利用ならmTLS/ネットワーク境界サイドチャネルの署名などで補強を検討。(Model Context Protocol)

1.2.2 Cerbos とは

  • Cerbos はアプリから認可判断を外出し(PDP: Policy Decision Point)するためのオープンソース基盤です。アプリは 「だれ(principal)/なに(resource)/なにをする(action)」 という三点セットを Cerbos に送り、許可/拒否を受け取ります。代表的な REST エンドポイントは /api/check/resources で、ここに JSON を投げると一括で判定できます。
  • ポリシーは YAML で記述し、条件式は Google CEL(Common Expression Language)を用いて ABAC(属性ベース制御)を簡潔に表現できます。ローカルで試せる CEL REPL も同梱されており、条件の挙動をすばやく検証できます。さらにPolicy Outputsを使えば、拒否理由や補足情報をアプリに返すことも可能です。(Policy Outputs)
  • セットアップは Docker 1 コマンドで起動でき、デフォルトで HTTP:3592 / gRPC:3593 を公開します(クイックスタート手順あり)。(Cerbos API)

使いどころ(本記事の文脈):MCP ツール実行の直前に、principal / resource(attr: client, tool, args) を Cerbos に送り、deny なら実行をブロックするガードを置く。


1.2.3 OPA(Open Policy Agent)とは

  • OPA は汎用のポリシーエンジンで、ポリシーをアプリ本体から切り離し、Rego という宣言的言語で記述します。Rego は JSON などの構造化データに対して柔軟な条件を書けるのが特徴です。
  • アプリは Data API(/v1/data/... を通じて OPA に input を渡し、評価結果(true/false だけでなく任意の構造化JSON)を受け取ります。これにより、許可/拒否だけでなく、拒否理由や次のアクション案などを機械可読に返す設計も可能です。(OPA REST API; OPA Intro)
  • OPA はアプリ内サイドカーデーモンとして動作し、ポリシーの外部化高速なインメモリ評価を通じて、分散システム全体の一貫したガバナンスを実現します。

使いどころ(本記事の文脈):MCP サーバから input = {principal, client, tool, args} を OPA に送り、RegoRBAC/ABAC を表現。deny の際は理由を構造化して返し、監査や UX に活用。


1.3 🔗 リポジトリ(全文・手順はこちら)

記事中のコードは骨格のみを掲載しています。
全文・更新は GitHub 側を正としてください。


2. 作って動かす(まず deny → 最小 allow → 直前ガード)

この章は 手を動かして“手応え”を得る ことに全振りします。
流れは 「まず全部denyを体験 → 最小allowに育てる → FastMCPに“直前ガード”として組み込む」 の三本立てです。
専門用語も最小限にします(RBAC=ロールベース、ABAC=属性ベース)。🧪


2.1 ゴールと共通の「入力」(PDPに投げる形)

まずは何を投げるかをイメージで共有します。Cerbos/OPAのどちらでも概念は同じです。

{
  "principal": { "id": "user-123", "roles": ["dev"] },
  "client":    { "name": "claude-desktop", "version": "1.12.3" },
  "tool":      { "name": "aws.ec2.start_instances" },
  "args":      { "region": "ap-northeast-1", "instance_ids": ["i-abc"] },
  "env":       { "ip": "203.0.113.10" }
}
  • Cerbosprincipal / resource.attr / action にマップ
  • OPA:そのまま input

ポイント:RBAC(roles)+ABAC(client/tool/args) の素材をこの形で渡します。


2.2 最小セットを用意して起動(必要最小限の雰囲気)

sample-mcp-authz/
├─ server/                 # FastMCP(authorize呼び出しを仕込む)
├─ cerbos/                 # YAML + CEL(RBAC/ABAC)
└─ opa/                    # Rego(RBAC/ABAC)

起動の最短ルート(抜粋)

docker compose up --build -d     # cerbos/opa/mcp をまとめて起動

ポート:Cerbos 3592(HTTP) / 3593(gRPC)、OPA 8181Cerbos API, OPA REST API


2.3 Step 1:まずは全部 deny を出してみる

最初は安全側(deny) で挙動を掴みます。

OPA(最小)

package mcp.authz
default allow = {"result": false, "reasons": ["default deny"]}

呼び出し(サンプル):

POST /v1/data/mcp/authz/allow
Content-Type: application/json

{ "input": { ...2.1のJSON... } }

期待結果:{"result":{"result":false,...}}

Cerbos(考え方)
一致するルールが無い=deny。極小ポリシーから条件を外すと EFFECT_DENY を確認できます。


2.4 Step 2:最小 allow に育てる(RBAC+ABAC)

Cerbos(YAML + CEL:最小イメージ)

resourcePolicy:
  resource: "mcp.tool"
  rules:
    - actions: ["invoke"]
      effect: EFFECT_ALLOW
      roles: ["dev","admin"]  # RBAC
      condition:
        match:
          all:
            of:
              - expr: request.resource.attr.client.name in ["claude-desktop","cursor"]
              - expr: request.resource.attr.tool.name.startsWith("aws.")
              # args の詳細チェックは GitHub の全文へ

最小呼び出し(イメージ):

POST /api/check/resources
{
  "principal": { "id": "user-123", "roles": ["dev"] },
  "resources": [{
    "resource": {"kind":"mcp.tool","attr":{ "client": {...}, "tool": {...}, "args": {...} }},
    "actions": ["invoke"]
  }]
}

期待結果:results[0].actions.invoke == "EFFECT_ALLOW"

OPA(Rego:最小イメージ)

package mcp.authz
default allow = {"result": false}
allow = {"result": true} {
  input.principal.roles[_] == "dev"                     # RBAC
  input.client.name == "claude-desktop"                 # ABAC: client
  startswith(input.tool.name, "aws.")                   # ABAC: tool
}

期待結果:{"result":{"result":true}}


2.5 Step 3:FastMCPに “直前ガード” として組み込む

概念:ツール実行の直前authorize(...) を呼び、denyなら止める

# server/app.py(最小イメージ。実装はGitHub全文)
def authorize(principal, client, tool, args) -> dict:
    # backend=cerbos|opa を内部で切替
    return {"allowed": True, "reasons": []}

@mcp.tool()
def start_instances(region: str, instance_ids: list, ctx: Context) -> dict:
    principal = {"id":"user-123","roles":["dev"]}
    client    = {"name": "claude-desktop", "version": "1.12.3"}  # clientInfo 由来
    tool      = {"name": "aws.ec2.start_instances"}
    args      = {"region": region, "instance_ids": instance_ids}
    decision  = authorize(principal, client, tool, args)
    if not decision["allowed"]:
        raise PermissionError("Denied by policy")
    return {"started": instance_ids, "region": region}

2.6 動かして“手応えチェック”

最短3ステップ(詳細はREADMEに集約):

  1. 起動
docker compose up --build -d
  1. クライアント接続(MCP Inspector / Claude Desktop)
  2. 呼び出し(例)
{"name":"start_instances","arguments":{"region":"ap-northeast-1","instance_ids":["i-abc"]}}

→ 条件に合えば allow、外せば deny(Cerbos: EFFECT_DENY / OPA: {"result":false}


2.7 実運用のコツ(ゼロトラストっぽく)

  • clientInfoの信用度:clientInfoは申告値mTLS/ネットワーク境界/署名付きヘッダなどで“本人確認”を強くする。
  • 安全側に倒す:PDP障害時は fail-closed(止める)か 限定allow(超制限モード)か、方針を決めておく。
  • 理由を返す:Cerbosの Policy Outputs や OPAの構造化レスポンス理由コードを返すと、監査とUXが楽。
  • テスト文化:CerbosのPlay/cerbosctl、OPAの opa testユニットテストを回す。
  • 引数スキーマ:MCPツール定義とPDP側の期待形を一元管理(ズレたらdenyに倒す)。
  • ポート/公開の明示:Cerbosは HTTP:3592 / gRPC:3593Cerbos API)、OPA は Docker で外部から評価するなら --addr=0.0.0.0:8181OPA REST API)を付ける。
  • ライフサイクル順守:MCPは**initialized 通知前に tools/list 等へ応答しない**のが仕様。サーバ側で順序を強制し、誤実行を防ぐ。
  • バージョン固定:本番は ghcr.io/cerbos/cerbos:0.46.0Cerbos v0.46.0 Release)、openpolicyagent/opa:1.5.1 のように具体的なバージョンタグで運用する(メジャータグより堅牢)。

2.8 Cerbos or OPA?(ざっくり比較)

観点 Cerbos(認可特化) OPA(汎用ポリシー)
書き味 YAML + CEL(直感的) Rego(柔軟だが学習コストあり)
API /api/check/resources /v1/data/...
はじめやすさ 速い(Docker一発) 速い(Docker一発)
返却 allow/deny + Outputs(理由) 任意の構造化JSON(理由もOK)
強み RBAC+ABACの定番がサクッと書ける 部分評価・データ駆動が強力

迷ったら:まずCerbosで“型”を掴む → OPAで高度化の順が学びやすいです。🙆


3. まとめと今後

まとめ

  • まずは denyファースト、必要なものだけ 最小allow に育てる。
  • 判定材料は 「誰/クライアント/ツール/引数」 の4点で十分。
  • FastMCPの“実行直前” でPDPに問い合わせ、ダメなら 止める
  • Cerbos(YAML+CEL)/OPA(Rego)どちらでも実現できる。

今後、考えていくべきこと

  • clientInfoの真正性:mTLS・ネットワーク境界・署名付きヘッダの現実解、という課題。
  • PDP障害時のふるまい:fail-closedか“超制限モード”か、という課題。
  • 引数スキーマの一元管理:ツール定義とポリシー側の型合わせ・自動生成、という課題。
  • 理由コード/監査:可観測性とプライバシーのバランス、という課題。
  • 性能と配置:レイテンシ、サイドカー/キャッシュ、SLOの取り方、という課題。
  • Human-in-the-loop:どのツールに二重承認を入れるか、使い勝手との折り合い、という課題。

小さく始め、必要なところから深掘りしていくのが良いと考えています。

参考リンク

  • MCPの概要/ツールの概念:modelcontextprotocol.io(ツールの定義・Quickstart)(Model Context Protocol)
  • MCPのライフサイクルとclientInfo(例):Architecture/Lifecycleの例・実装Issueログ (Model Context Protocol, GitHub)
  • Cerbos:API・クイックスタート・条件(CEL)・ベストプラクティス・ログ/管理API (docs.cerbos.dev)
  • OPA:REST API・Rego言語・ハンズオン(HTTP API認可)(openpolicyagent.org)
  • FastMCP(Python SDK):サーバ実装の最小例 (GitHub)
  • Inspector/接続:MCP Inspector・Claude Code/Desktopの接続ガイド (GitHub, Anthropic, Model Context Protocol)

Discussion