2026年9月4日 金曜日
AI時短ラボ
検証· 約14

OSキーチェーンが無い環境でもMCP InspectorがOAuthの秘密を保持できるように──AES-256-GCM暗号化と「壊れたら書き込まない」設計

MCP Inspector v2.4.0に、OSキーチェーンにアクセスできないホスト(コンテナ・Android/Termux・最小構成Linux)向けの新しいSecretStore実装が追加された。ファイル1本をAES-256-GCMで暗号化する設計で、実装したPR本文には脅威モデルの判断理由まで詳しく書かれている。

OSキーチェーンが無い環境でもMCP InspectorがOAuthの秘密を保持できるように──AES-256-GCM暗号化と「壊れたら書き込まない」設計
執筆・編集:
目次

MCP InspectorはOAuthのクライアントシークレットや、stdioサーバー起動時のenv:値を、通常はOSのキーチェーン(macOSのKeychain、WindowsのCredential Managerなど)に保存する。だが公開コンテナやAndroid/Termux、最小構成のLinuxのように、そもそもOSキーチェーンに到達できない環境では、これまで秘密を永続化する手段が無かった。MCP Inspector v2.4.0(2026年8月26日公開)に、この穴を埋める実装が追加されたことを、マージ済みPR #2076の本文で確認した。

「正しい失敗」はしていたが、永続化の手段自体が無かった

PR本文はこれまでの状態をこう説明している。

A host with no reachable OS keychain — the published container (no D-Bus session, #1848), Android/Termux (no platform binary, #1905), a minimal Linux install without libsecret — could not persist an OAuth client secret or a stdio env: value at all. KeyringSecretStore degraded into a store that silently holds nothing: getnull, delete → no-op, set → 503. Correct failure behavior, but it left the documented, supported way to run the Inspector with no secret persistence whatsoever.

(到達可能なOSキーチェーンを持たないホスト——公開コンテナ(D-Busセッション無し)、Android/Termux(プラットフォームバイナリ無し)、libsecretの無い最小構成Linux——は、OAuthクライアントシークレットやstdioのenv:値をまったく永続化できなかった。KeyringSecretStoreは、何も静かに保持しないストアへと退化していた:getnulldeleteは何もしない、setは503を返す。失敗の挙動としては正しいが、これによりInspectorをドキュメント通りの・サポートされた方法で動かしても、秘密の永続化がまったくできない状態が残っていた)

ファイル1本をAES-256-GCMで暗号化する設計

新しく追加されたfile-secret-store.tsは、~/.mcp-inspector/secrets.jsonというパーミッション0600(所有者のみ読み書き可)の1つのJSONファイルを使う。

core/auth/node/file-secret-store.ts — one 0600 JSON document at ~/.mcp-inspector/secrets.json, encrypted with AES-256-GCM whenever MCP_INSPECTOR_SECRET_KEY is set (scrypt, per-file random salt, so the same passphrase yields a different key per file and a precomputed table buys nothing).

core/auth/node/file-secret-store.ts——~/.mcp-inspector/secrets.jsonにある権限0600の1つのJSON文書。MCP_INSPECTOR_SECRET_KEYが設定されている場合はAES-256-GCMで暗号化される(scrypt、ファイルごとにランダムなソルトを使うため、同じパスフレーズでもファイルごとに異なる鍵になり、事前計算テーブルは役に立たない))

暗号化の設計で「あえてそうした」と説明されている判断が2つある。

The whole map is encrypted as a unit, not value-by-value. The account names are ${serverId}:${field}, so a per-value scheme would leave a readable index of which servers you use and which of them you hold an OAuth client secret for. That index is worth roughly as much to an attacker as some of the values.

A decrypted payload is validated, not cast. GCM proves the bytes are authentic, not that they parse to the shape we want. An array passes typeof === "object", accepts the named assignment, and is then serialized by JSON.stringify with every named property dropped — so set would resolve having written a file without the secret it was handed.

(マップ全体を1単位として暗号化しており、値ごとには暗号化していない。アカウント名は${serverId}:${field}という形式なので、値ごとに暗号化する方式だと、どのサーバーを使っていて、そのうちどれについてOAuthクライアントシークレットを保持しているかが読み取れる索引が残ってしまう。その索引は、攻撃者にとって値そのものと同じくらいの価値を持ちうる。復号したペイロードは検証されるのであって、型キャストされるだけではない。GCMはバイト列が本物であることを証明するが、それが期待する形にパースできることまでは証明しない。配列はtypeof === "object"を通過し、名前付き代入も受け入れてしまうが、その後JSON.stringifyでシリアライズされる際に、名前付きプロパティは全て失われる——つまりsetは、渡されたはずの秘密を書き込まないまま成功したことになってしまう)

パスフレーズの強度についても明確な警告がある。「パスフレーズは高エントロピーで、生成されたものでなければならず、選んだものであってはならない。ソルトは事前計算を防ぐが、推測には無力で、scryptのコストは意図的に数ミリ秒に抑えられている(読み書きのたびに鍵導出が走るため)。secrets.jsonを入手した者は、候補を素早くオフラインで試せる」という一文もPR本文にはあった。

どのストアが選ばれるか──コンテナはマウントの有無で挙動が変わる

PR差分の実装コード(chooseFallbackKind関数)を読むと、キーチェーンが使えない場合に「ファイル」と「メモリ(プロセス終了で消える)」のどちらを選ぶかは、単純にコンテナかどうかだけでなく、ボリュームがマウントされているかどうかで決まっている。

環境 選ばれるストア 永続性
キーチェーンが使える通常環境 Keyring(OS標準) 永続
キーチェーンが無く、ボリュームがマウントされたコンテナ File(~/.mcp-inspector/secrets.json 永続
キーチェーンが無く、ボリューム未マウントのコンテナ Memory(インメモリ) プロセス終了で消える
キーチェーンが無いその他の環境(Android/Termux、最小構成Linuxなど) File 永続

コード中のコメントは、この設計の理由を「書き込み可能レイヤー内のファイルは--rmやイメージ更新のたびに消えるため、実現できない永続性を約束するくらいなら、最初から約束しない方がいい」と説明している。動作は環境変数MCP_INSPECTOR_SECRET_STOREkeyringfilememoryのいずれか)で明示的に上書きもできる。

暗号化キーを設定しない場合は平文で保存される

PR差分を確認すると、MCP_INSPECTOR_SECRET_KEYを設定しない場合の挙動が明記されていた。

A file-backed store is unencrypted unless you give it a key. [...] Without it the file is still 0600, but the values are readable to anyone who can read the file — which the startup log and the settings footer both say, every session, in a warning tone.

(ファイルベースのストアは、鍵を与えない限り暗号化されない。〔中略〕鍵が無くてもファイルの権限は0600のままだが、その値はファイルを読める人なら誰でも読める状態になる——これは起動ログと設定画面のフッターの両方に、毎セッション、警告として表示される)

つまりMCP_INSPECTOR_SECRET_KEYは暗号化を有効にするための任意設定で、設定しなければファイルパーミッション(所有者のみ読み書き可)だけが防御線になり、中身は平文のJSONとして保存される。後からパスフレーズを設定した場合は「次の書き込み時に、既存の平文ファイルがその場で暗号化版にアップグレードされる」ため安全だが、逆にパスフレーズを変更・紛失した場合は前述の通り「復号できないファイルへの書き込みを拒否する」設計のため、元のパスフレーズを復元するか、secrets.jsonを削除して値を入力し直す必要がある、とPR差分は説明している。

「壊れたファイルには書き込まない」という設計判断

もう1つの明記されたルールがある。

A file that can no longer be decrypted refuses to be written. Reads go quiet (null, per the interface's read tolerance), but writing would replace a file full of still-valid secrets with one holding a single new one — destroying data to satisfy a request that was only ever additive.

(もはや復号できなくなったファイルは、書き込みを拒否する。読み取りは静かに失敗する(インターフェースの読み取り許容仕様に従いnullを返す)が、書き込みを許してしまうと、まだ有効な秘密がたくさん入ったファイルを、新しい秘密1つだけが入ったファイルで置き換えてしまう——本来は追加のみのはずのリクエストを満たすために、データを破壊することになる)

破損したファイルへの書き込みを拒否することで、1件の新しい秘密を保存しようとした操作が、既存の全ての秘密を消してしまう事故を防ぐ設計だ。

実際にコンテナで動かして確認したわけではない

本記事はPR #2076のPR本文・実装コードの差分(.diff)・発端になったIssue #1848を実際に開いて確認した内容にもとづく。この記事を書いている自分の手元で、コンテナ環境(マウントあり・なしの両方)やAndroid/Termux環境を実際に用意し、SecretStoreの暗号化・復号やストア選択のロジックを動かして再現する検証は行っていない。chooseFallbackKind関数のロジックやMCP_INSPECTOR_SECRET_STORE環境変数の挙動は、この記事ではコードの差分を読んで説明しているのであって、実行して確かめたものではない。キーチェーンが後から使えるようになった場合にファイルの秘密がキーチェーンへ吸い上げられる(absorbFileSecretsIntoKeyringという関数名が差分中に見つかった)挙動についても、関数名以上の詳細はこの記事では確認していない。

関連記事: MCP(Model Context Protocol)とは / 生成AIのセキュリティリスク──企業が押さえるべき6つの脅威と対策 / MCP Inspectorのv2が標準に

感想・指摘はコメント欄へ。

シェア: ポスト はてブ

出典・参照資料

AIニュースの解説を動画でも

YouTubeでは注目ニュースの背景を解説し、Xでは新着記事をお知らせしています。

コメント

まだコメントはありません。最初のコメントを書いてみませんか?

AIについて聞きたいことはありますか?

質問箱で無料で受け付けています。回答は公開され、他の方の参考にもなります。

質問箱を見る →

新しい記事をメールで受け取る

AIの新しい発表を、出典付きで整理して届けます。

関連記事

GitHubにすら置かれていないコーディングエージェント「kaagum」──Guile言語・自前UIなし・ACP専用という徹底ぶりの記事画像
検証09.02読了14

GitHubにすら置かれていないコーディングエージェント「kaagum」──Guile言語・自前UIなし・ACP専用という徹底ぶり

出典 ─ kaagum: Tiny, security
1.52MBのバイナリが3.5msでコンテナを起動する──ルートレスサンドボックス「kern」をAI生成コードの実行にどう使うかの記事画像
検証09.02読了15

1.52MBのバイナリが3.5msでコンテナを起動する──ルートレスサンドボックス「kern」をAI生成コードの実行にどう使うか

出典 ─ getkern/kern
AI Skillsに「品質ゲート」を通す──NVIDIAが公開したオープンソース評価基盤SkillEvaluatorを読むの記事画像
検証09.02読了14

AI Skillsに「品質ゲート」を通す──NVIDIAが公開したオープンソース評価基盤SkillEvaluatorを読む

出典 ─ NVIDIA/SkillEvaluator
『26.1%のスキルに脆弱性』──外部Agent Skillsを入れる前に走らせるNVIDIAのセキュリティスキャナ「SkillSpector」の記事画像
検証09.02読了14

『26.1%のスキルに脆弱性』──外部Agent Skillsを入れる前に走らせるNVIDIAのセキュリティスキャナ「SkillSpector」

出典 ─ NVIDIA/SkillSpector RE
AIエージェントの`rm -rf`を実行前に止める「Doberman」──自己申告ベンチマークを読むと見えてくる限界の記事画像
検証09.04読了12

AIエージェントの`rm -rf`を実行前に止める「Doberman」──自己申告ベンチマークを読むと見えてくる限界

出典 ─ fu351/Doberman-Core
2.4億DL超のRustクレートarrayrefが乗っ取られた4時間──base64のC2アドレスと『非yank版へどうぞ』の罠の記事画像
業界09.02読了15

2.4億DL超のRustクレートarrayrefが乗っ取られた4時間──base64のC2アドレスと『非yank版へどうぞ』の罠

出典 ─ RustSec Advisory DB Is
人気Rustクレートarrayrefが乗っ取られ、ビルド時にマルウェアを実行していた──245百万DLの依存先で何が起きたかの記事画像
研究09.01読了16

人気Rustクレートarrayrefが乗っ取られ、ビルド時にマルウェアを実行していた──245百万DLの依存先で何が起きたか

出典 ─ SafeDep公式ブログ「Malicious
モデル署名だけじゃない──OpenSSFのAI/MLセキュリティWGが同時に動かす7つのプロジェクトの記事画像
研究09.01読了19

モデル署名だけじゃない──OpenSSFのAI/MLセキュリティWGが同時に動かす7つのプロジェクト

出典 ─ GitHub: ossf/ai-ml-sec