gateway について
gatewayは、MCPサーバーの前段に置く認証リバースプロキシです。Agentのツール呼び出しを制限し、ZKP認証情報を検証して署名付きレシートを生成します。
主な機能
- 01AgentのZKP認証情報の検証
- 02ツール単位の権限ポリシー適用
- 03nonceリプレイの検査
- 04署名付きハッシュチェーンレシートの生成
- 05Redisによるnonceストア共有
活用シーン
- 01MCPサーバー到達前に未認可のツール呼び出しを遮断
- 02レシートをファイル、標準出力、webhookへ出力
- 03Redisで複数インスタンス間のリプレイ保護を共有
- 04収集した署名付きレシートを個別に検証
MCP 対応
MCPサーバーのリバースプロキシとして、tools/call要求を検証し、認可後に転送します。他のJSON-RPCメソッドは認証せずに転送します。
設定
ローカルサーバーの設定はどのクライアントでも共通です。コピーしてクライアントの MCP 設定ファイルに貼り付けてください。
ツール
ローカルサーバーのツールはマシン上で起動して初めて取得できます。メンテナーはまだツール一覧を提供していません。
詳細
@bolyra/gateway
Bolyra MCP Auth Gateway -- 上流の MCP サーバーにリクエストを転送する前に、エージェントの ZKP 認証情報を検証するスタンドアロンのリバースプロキシです。
MCP サーバーの前段に @bolyra/gateway を配置すると、エージェントの権限を検証し、リプレイを防止し、委任されたスコープを適用して、すべてのツール呼び出しについて署名付き監査ログを記録できます。
クイックスタート
# Install
npm install @bolyra/gateway
# Create a config file (optional)
cat > gateway.yaml << 'EOF'
target: http://localhost:3000/mcp
port: 4100
devMode: true
EOF
# Run
npx @bolyra/gateway --config ./gateway.yaml
または、CLI フラグを指定して直接実行します。
npx @bolyra/gateway --target http://localhost:3000/mcp --dev --port 4100
仕組み
+----------------------------------+
| @bolyra/gateway |
Agent -------->| |--------> Upstream MCP Server
(with proof | 1. Parse Authorization header | (unmodified)
bundle) | 2. verifyBundle() from @bolyra/mcp|
| 3. checkToolPolicy() |
| 4. Nonce replay check |
| 5. Emit signed receipt |
| 6. Proxy request + X-Bolyra-* hdrs|
| |
| Config: gateway.yaml |
| Receipts: ./receipts/ or stdout |
+----------------------------------+
リクエストのルーティング:
GET /healthz-- ゲートウェイのステータスを返します(インターセプトされ、プロキシされません)- JSON-RPC のメソッドが
tools/call以外 -- 認証なしで上流に転送されます - JSON-RPC のメソッドが
tools/call-- 認証を検証し、許可されている場合に上流へ転送します
CLI リファレンス
npx @bolyra/gateway [options]
Options:
--target <url> Upstream MCP server URL (required unless in config)
--port <number> Gateway listen port (default: 4100)
--config <path> Path to gateway config file (default: ./gateway.yaml)
--dev Enable Bolyra Core mode (classical checks: tool policy,
nonce replay, signed receipts, credential binding
against registered credentials; ZK proof verification
off. Without registered credentials, permission claims
are self-asserted — see Credential Binding below)
--credentials <path> Credentials file (YAML/JSON map: commitment ->
{ permissionBitmask, expiryTimestamp? }); overrides
the config's credentials section
--receipt-dir <path> Directory for receipt JSON files (default: ./receipts/)
--receipt-stdout Write receipts to stdout (NDJSON)
--no-receipts Disable receipt generation
--network <name> Bolyra network (default: base-sepolia)
--help Show help
--version Show version
CLI フラグは設定ファイルの値を上書きします。
設定ファイルのリファレンス
# gateway.yaml
target: http://localhost:3000/mcp
port: 4100
network: base-sepolia
# Bolyra Core mode: classical checks only, ZK proof verification skipped
devMode: false
# Registered credentials (see "Credential Binding" below).
# Core mode (devMode: true): claims are checked against this registry —
# unknown commitments, forged masks, and expired credentials are denied.
# Production mode: this map becomes the resolveCredential source for
# verifyBundle's Poseidon3 scopeCommitment binding (expiryTimestamp
# required per entry).
# type: registry (on-chain resolution) is not supported by the packaged
# gateway yet and fails validation — use the library resolveCredential
# option for custom resolution.
credentials:
type: static
map:
"48201394857102938471029384": # credential commitment (decimal)
permissionBitmask: 3 # READ_DATA | WRITE_DATA
expiryTimestamp: "1893456000" # unix seconds; optional in Core mode
# Per-tool policies
tools:
write_file:
requireBitmask: 2 # WRITE_DATA (0b10)
delete_file:
requireBitmask: 6 # WRITE_DATA + FINANCIAL_SMALL (0b110)
maxChainDepth: 0 # Direct credentials only
transfer_funds:
requireBitmask: 28 # FINANCIAL_* (0b11100)
minScore: 90
# Nonce replay protection
nonce:
store: memory # "memory" (default) or "redis"
maxProofAge: 300 # seconds
# Redis config (required when store: redis)
# redis:
# url: ${REDIS_URL} # required
# keyPrefix: "bolyra:nonce:" # optional, default shown
# connectTimeout: 5000 # optional, ms
# Receipt signing — every allow/deny decision is ES256K-signed (see
# "Signed Receipts" below). Without privateKey an ephemeral key is
# generated at startup; set it for a stable, pinnable signer address.
receipts:
enabled: true
issuer: "my-gateway"
keyId: "k1"
privateKey: "${BOLYRA_RECEIPT_KEY}" # 32-byte hex secp256k1 key, env var substitution
output: file # "file", "stdout", or "webhook"
dir: ./receipts/
# Health check
health:
enabled: true
path: /healthz
# Optional HMAC signing for X-Bolyra-* headers
# hmac:
# secret: "hex-encoded-shared-secret"
文字列値では、${VAR_NAME} 構文を使って環境変数を置換します。
ライブラリ API
独自のサーバーにゲートウェイミドルウェアを組み込む場合:
import {
createGatewayMiddleware,
createGatewayProxy,
loadConfig,
injectBolyraHeaders,
computeHmac,
verifyHmac,
createReceiptWriter,
createHealthHandler,
} from '@bolyra/gateway';
import type {
GatewayConfig,
GatewayMiddlewareOptions,
GatewayRequest,
} from '@bolyra/gateway';
// Option 1: Full proxy server
const config = loadConfig({ target: 'http://localhost:3000/mcp', dev: true });
const server = createGatewayProxy({ config });
server.listen(4100);
// Option 2: Just the middleware (for custom servers)
const middleware = createGatewayMiddleware({ config });
// Use in your request handler:
// const authorized = await middleware(req, res, toolName);
エクスポート
| エクスポート | 説明 |
|---|---|
createGatewayProxy(options) | 完全なリバースプロキシ HTTP サーバーを作成します |
createGatewayMiddleware(options) | 認証検証ミドルウェアのみを作成します |
loadConfig(flags?) | ゲートウェイ設定を読み込み、検証します |
injectBolyraHeaders(authCtx, receiptId?) | X-Bolyra-* ヘッダーを構築します |
computeHmac(headers, secret) | X-Bolyra-* ヘッダーに HMAC-SHA256 署名を付与します |
verifyHmac(headers, secret, hmac) | X-Bolyra-* ヘッダーの HMAC を検証します |
createReceiptWriter(config) | 差し替え可能なレシート出力を作成します |
createGatewayReceiptSigner(config, override?) | ES256K レシート署名者を解決します(設定済みキーまたは一時キー) |
createHealthHandler(config) | /healthz エンドポイントハンドラーを作成します |
extractToolName(body) | JSON-RPC ボディからツール名を抽出します |
loadCredentialsFile(path) | --credentials ファイルを読み込みます(マップ単体、またはセクション全体) |
buildCredentialRegistry(credentials) | 静的な認証情報セクションをコンパイルして検索レジストリを作成します |
checkCredentialBinding(bundle, authCtx, registry) | Core モードで登録済み認証情報を検査します(Credential Binding を参照) |
createStaticCredentialResolver(credentials) | 本番環境での scopeCommitment バインディング用に、静的な resolveCredential を作成します |
hasStaticCredentials(credentials) | 使用可能な静的認証情報マップが設定されている場合に true を返します |
RedisNonceStore | 複数インスタンスのデプロイ向け Redis バックエンドの NonceStore |
Redis Nonce Store(v0.2.0+)
複数インスタンスのデプロイで共有のノンスリプレイ保護を使用するには、Redis を使用します。
# gateway.yaml
nonce:
store: redis
maxProofAge: 300
redis:
url: ${REDIS_URL}
keyPrefix: "bolyra:nonce:" # optional
ライブラリでの使用方法:
import { createGatewayMiddleware, RedisNonceStore } from '@bolyra/gateway';
const store = new RedisNonceStore({ url: 'redis://localhost:6379' });
const middleware = createGatewayMiddleware({ config, nonceStore: store });
// Graceful shutdown
process.on('SIGTERM', () => store.close());
フェイルクローズ動作: Redis に接続できない場合、ゲートウェイは HTTP 500 を返し、リクエストを決して通過させません。重要な依存先として、Redis の可用性を監視してください。
X-Bolyra-* ヘッダー
リクエストの検証に成功すると、ゲートウェイは次のヘッダーをプロキシ対象のリクエストに挿入します。
| ヘッダー | 値 |
|---|---|
X-Bolyra-Verified | true |
X-Bolyra-DID | did:bolyra:{network}:{commitment} |
X-Bolyra-Score | 検証スコア(0-100) |
X-Bolyra-Permissions | 有効な権限ビットマスク(10 進数) |
X-Bolyra-Chain-Depth | 委任チェーンの深度(0 = 直接) |
X-Bolyra-Receipt-ID | この判定の署名付きレシート ID(レシートが有効な場合) |
X-Bolyra-HMAC | HMAC-SHA256 署名(HMAC が設定されている場合) |
上流サーバーは、Bolyra をインポートせずに、これらのヘッダーをロギングや監査に使用できます。
署名付きレシート(v0.3.0+)
すべての tools/call の判断(許可と拒否、開発モードと本番環境)で、ES256K 署名付きレシート(@bolyra/receipts の SignedReceipt:secp256k1 + keccak256、Ethereum 互換のリカバリー)が生成されます。使用可能な証明バンドルがまったくない場合の拒否も含まれます。Authorization ヘッダーが欠落している、または形式が不正な場合でも、署名付きの匿名拒否レシートが残るため、監査証跡に未署名の欠落が生じません。
各レシートには、判定(decision.allowed)、人間が読める reasonCode(どのツールで、どの権限が必要で、どの権限を持っていたか)、判明している場合はエージェント DID、スコア、権限ビットマスク、委任チェーンの深度、提示された証明資料のハッシュ、判定時刻が含まれます。レシートを編集すると署名が無効になります。
署名キーの解決順序:
receiptSignerオプション(ライブラリを埋め込む場合)- 設定の
receipts.privateKey(本番環境で推奨) - それ以外の場合は、起動時にエフェメラルキーが生成されます。レシートは個別に検証可能なままです。署名者のアドレスはすべての署名から復元できますが、再起動のたびにアドレスが変わります。CLI は起動バナーに署名者のアドレスを表示し、
file出力モードでは監査担当者が信頼アンカーを固定できるよう、{receipt-dir}/signer.jsonに保存します。
独立した検証に必要なのはレシート(および必要に応じて固定済みの署名者アドレス)だけです。ゲートウェイもデータベースも必要ありません。
import { verifyReceipt } from '@bolyra/receipts';
const receipt = JSON.parse(fs.readFileSync('receipts/2026-07-10/....json', 'utf8'));
verifyReceipt(receipt); // true — signature + payload hash check
verifyReceipt(receipt, pinnedSigner); // true — also pins the signer address
ハッシュチェーン付きレシート(v0.5.0+)
署名によって各レシートの改ざんを検知できるようになります。ハッシュチェーンによって、レシートのストリームの改ざんも検知できるようになります。ゲートウェイが署名するすべてのレシートには、署名対象ペイロード内に chain: { seq, prevReceiptHash } が含まれ(seq はゲートウェイプロセスごとに 0 から始まり、最初のレシートは 32 個のゼロバイトからなるジェネシスセンチネルにリンクされます)、さらに次のレシートがリンクするトップレベルの receiptHash も含まれます。収集したログ内でレシートを削除、並べ替え、挿入すると、残りの署名が個別にはすべて有効であっても、チェーン検証に失敗します。
# stdout mode: collect NDJSON to a file, verify signatures + chain
bolyra receipt verify-chain receipts.ndjson --signer 0xPinnedSigner
監査担当者向けの注意事項:
- ゲートウェイプロセスごとにチェーンは 1 つです。 再起動すると seq 0 から新しいチェーンが始まります。プロセスの実行ごとに収集したログを分けるか、各実行のセグメントを個別に検証してください。ファイルの途中での再起動は
chain-restartとして報告されます。 - ファイルモードではレシートごとに 1 つの JSON ファイルを書き込みます。チェーンフィールドによって引き続き順序とリンクが保持されます(検証するには seq 順に連結してください)。
- ログだけでは末尾の切り詰めを検知できません。 チェーン検証で証明できるのは、ログ内で削除、並べ替え、編集が行われていないことだけです。どのレシートの後でログが切られていても、内部的には整合性が保たれます。
verify-chainが出力するヘッドハッシュ/レシート数を固定し、--expect-head/--expect-countを指定して再検証してください。これらのチェックポイントをどのように、どのくらいの頻度でアンカーするか(オブジェクトロックストレージ、透明性ログ、チェーン)は、エンタープライズ向けに設定可能なデプロイポリシーです。 - 0.5.0 より前のレシートにはチェーンフィールドがありません。
--allow-unchainedを指定すると、それらを先頭のプレフィックスとして受け入れ(署名は引き続き検証)、残りをチェーン検証します。チェーン付きレシートの後にチェーンフィールドのないレシートが続く場合は、常に失敗します。挿入された行である可能性があるためです。
領収書の出力モード
| モード | 説明 |
|---|---|
file | 日付ごとにローテーションするディレクトリに JSON ファイルを保存します:receipts/2026-06-18/{timestamp}-{receipt-id}.json。また、固定された署名者アドレスを含む signer.json も保存します |
stdout | NDJSON を標準出力に出力します(領収書ごとに JSON 1 行) |
webhook | 設定された URL に HTTP POST します。認証ヘッダーは任意です |
すべてのモードはノンブロッキングです。書き込みや署名に失敗した場合はログに記録しますが、リクエスト処理を中断することはありません。署名キーは起動時にプローブ検証されます。例外的に実行時の署名に失敗した場合は、決定内容を "unsigned": true と明示的にタグ付けした生の JSON レコードとして記録します(署名エラーも含まれます)。これにより監査側の利用者は欠落を検出できます。未署名のレコードが署名済みの領収書になりすますことは決してありません。
領収書には、ゲートウェイの最終的な決定が常に記録されます。認証には成功してもツールポリシーに違反したリクエストは、拒否の領収書になります。JSON として解析できても証明資料がまったく含まれていないバンドルは、フェイルクローズで拒否され(HTTP 401)、匿名の署名済み領収書が発行されます。
認証情報のバインド(v0.4.0+)
Core モード(--dev)では証明がモックされるため、バンドル内の権限マスクは呼び出し元が申告します。認証情報を登録すると、その申告が強制チェックの対象になります。これは examples/verified-actions-demo と同じ登録済み認証情報のパターンを、パッケージ化したものです。
- 未知のコミットメント → 401 + 拒否の署名済み領収書(
credential_unknown) - 申告されたマスク ≠ 登録済みの権限(偽造バンドル)→ 401 + 拒否の署名済み領収書(
credential_mismatch) - 委任チェーンが権限の範囲を超えて拡大 → 401(
credential_mismatch— スコープの縮小は一方向です) - 登録済みの有効期限を超過 → 401 + 拒否の署名済み領収書(
credential_expired)
すべての拒否はフェイルクローズで行われ、ほかのすべての決定と同様に領収書が発行されます。
開発用認証情報を登録する。 Core モードのコミットメントは、プラットフォームが割り当てる不透明な識別子です(本番環境のコミットメントは認証情報の Poseidon ハッシュです)。生成して登録します。
# 1. Generate a commitment
node -e "console.log(BigInt('0x' + require('crypto').randomBytes(16).toString('hex')).toString())"
# 2. Register it (credentials.yaml)
cat > credentials.yaml << 'EOF'
"48201394857102938471029384":
permissionBitmask: 3 # READ_DATA | WRITE_DATA
EOF
# 3. Run with binding enforced
npx @bolyra/gateway --target http://localhost:3000/mcp --dev --credentials ./credentials.yaml
コミットメント(および付与されたマスク)をエージェントに渡してください。エージェントの証明バンドルには、その識別情報とマスクを正確に含める必要があります。--credentials ファイルには、単純なマップ(上記)と設定セクション全体の形式(type: static、map:)のどちらも指定できます。gateway.yaml の credentials: セクションも同等です。
認証情報を登録しない場合、チュートリアルでは Core モードを引き続き手軽に利用できます。任意の申告が通ります(現行の動作)。ただし、ゲートウェイは起動時に警告し、すべての許可領収書に [credential-binding: none — permission claims self-asserted] というフラグを付けます。これにより監査側の利用者は、エフェメラルな領収書署名キーと同じ可視化パターンで、そのトレードオフを確認できます。
本番モードでは、同じ credentials セクションが resolveCredential の実装にコンパイルされ、verifyBundle の暗号学的バインドが有効になります。検証器は登録済み認証情報から Poseidon3(permissionBitmask, commitment, expiryTimestamp) を再計算し、証明の公開出力である scopeCommitment と一致することを要求します。偽造されたマスクでは有効な証明を生成できません。そのため、本番環境では各エントリに expiryTimestamp が必須です。ライブラリを埋め込む側は引き続き resolveCredential を直接渡すこともでき、その場合は設定セクションより優先されます。
セキュリティモデル
ゲートウェイは信頼境界です。エージェントは証明バンドルを提示し、ゲートウェイはそれを検証してから転送します。
デプロイに関するガイダンス:
- アップストリームの MCP サーバーを localhost またはプライベートネットワークにバインドします
- インターネットに公開するのはゲートウェイのみにします
- クライアントとゲートウェイ間で HTTPS を使用します
X-Bolyra-*ヘッダーがゲートウェイによって設定されたことを検証する必要がある場合は、HMAC 署名を設定します
対処する脅威:
- リプレイ攻撃(nonce ストア)
- 認証情報のすり替え(本番環境:Poseidon3 スコープコミットメントバインディング)
- Core モードでの偽造された権限要求(認証情報が設定されている場合の、登録済み認証情報とのバインディング)
- 委任の権限昇格(一方向のスコープ縮小、回路による強制)
- スコープのバイパス(ツールごとのビットマスクによる強制)
- ヘッダーインジェクション(オプションの HMAC 署名)
- 監査の回避(匿名の拒否を含む、すべての判断(許可・拒否)に対する ES256K 署名済みレシート —
verifyReceiptによる改ざん検知)
権限ビットマスクリファレンス
| ビット | 権限 | 値 |
|---|---|---|
| 0 | READ_DATA | 1 |
| 1 | WRITE_DATA | 2 |
| 2 | FINANCIAL_SMALL (<$100) | 4 |
| 3 | FINANCIAL_MEDIUM (<$10K) | 8 |
| 4 | FINANCIAL_UNLIMITED | 16 |
| 5 | SIGN_ON_BEHALF | 32 |
| 6 | SUB_DELEGATE | 64 |
| 7 | ACCESS_PII | 128 |
上位の権限は下位の権限を含みます(例:FINANCIAL_MEDIUM は FINANCIAL_SMALL を含みます)。
Docker
Node.js のセットアップを一切行わずに、ゲートウェイをコンテナとして実行できます。
GHCR からプル
docker pull ghcr.io/bolyra/gateway:latest
クイックスタート
# Bolyra Core mode -- classical checks, no circuits, no config file needed
docker run --rm -p 4100:4100 ghcr.io/bolyra/gateway \
--target http://host.docker.internal:3000/mcp --dev
本番環境
# Mount config file + set env vars
docker run -d \
-v $(pwd)/gateway.yaml:/etc/bolyra/gateway.yaml:ro \
-e REDIS_URL=redis://redis:6379 \
-e BOLYRA_RECEIPT_KEY=hex-encoded-key \
-p 4100:4100 \
ghcr.io/bolyra/gateway
ローカルでビルド
# From repo root
docker build -f Dockerfile.gateway -t bolyra-gateway:local .
# Run with local build
docker run --rm -p 4100:4100 bolyra-gateway:local \
--target http://host.docker.internal:3000/mcp --dev
# Verify health
curl http://localhost:4100/healthz
ポートを上書き
docker run --rm -p 8080:8080 ghcr.io/bolyra/gateway \
--target http://upstream:3000/mcp --port 8080 --dev
イメージの詳細
- ベース:
node:22-alpine - ユーザー:
bolyra(UID 1001、非 root) - ポート: 4100(デフォルト)
- ヘルスチェック: 30 秒ごとに
GET /healthz - 設定のマウント先:
/etc/bolyra/gateway.yaml - レシートディレクトリ:
/app/receipts/(書き込み可能) - アーキテクチャ:
linux/amd64、linux/arm64
イメージ名の後に渡した CLI フラグは、設定ファイルの値を上書きします。上記のCLI リファレンスを参照してください。
ライセンス
Apache-2.0
コメント
AI アプリケーションが外部のツールやデータソースに統一的な方法で接続するためのオープンなプロトコルです。クライアントはモデルとの対話を担い、MCP サーバーはツール・リソース・プロンプトテンプレートを提供します。標準メッセージでやり取りするため、1 つのサーバーを複数のクライアントで再利用できます。