🔐

MCPサーバーを社内に置く前に決めた3つのこと —— 認証・ロール・検査の順番

に公開

動いた。で、これをどこに置くのか

MCPサーバーを作ってみた人なら、公式のサンプルを動かすところまでは、たぶん詰まらずに行けると思います。ドキュメントどおりにやれば動きます。

問題はその次でした。動いた。で、これどこに置くのか。

置き場所は3つあります。自分のPC、社内のサーバー、インターネット。自分のPCに置いている限りは、誰も困りません。自分しか触れないからです。

でも、それでは仕事に使えません。社内の人に使ってもらうには、どこかに置く必要があります。

そして置いた瞬間に、認証をつけていないことに気づきます。

認証がなくても、エラーは起きない

ここで誤解しやすいのですが、認証がなくてもエラーは起きません。ちゃんと動きます。 鍵を持たずに叩いても 200 OK が返ってきて、道具の一覧が全部見えます。壊れていないので、誰も気づきません。

つまり、誰でも入れてしまう状態です。

そしてMCPには特有の怖さがあります。普通のデータベースなら、入れてもSQLが書けなければ何も取り出せません。MCPはAIが繋がっているので、自然言語で引き出せます。「先月の売上は?」と聞くだけです。

私が本番で動かしているMCPサーバーには、読み取り専用の道具が十数個あります。そこには経営に関わる数字が入っています。

経営数字が、社内の誰にでも、日本語で聞くだけで出てくる。 それが、認証をつけないということでした。

設計判断① 「権限がありません」と返さない

権限のない道具を呼ばれたとき、私のサーバーはこう返します。

Tool server_config not found

「権限がありません」ではありません。そんな道具はありません、です。

理由は2つあります。

1つ目。「権限がありません」と返すと、その道具が存在することが相手に伝わります。

存在が分かれば、次は「どうすれば呼べるのか」を探しに来ます。ヘッダーをいじる、別の鍵を試す。無いものは、狙われません。

2つ目。こちらは作る側の話です。

権限のチェックを各ツールの中に書く方式だと、ツールが増えるほど書く場所が増えます。10個あれば10箇所です。

人間は必ずミスをします。 そして厄介なのは、書き忘れてもエラーが出ないことです。その道具だけ誰でも使える状態になり、動いているので誰も気づきません。冒頭で書いたのと同じ構造です。

登録の段階で振り分ければ、書く場所は roles: ["admin"] の1行だけです。もし書き忘れたら、その道具は誰にも見えなくなります。

書き忘れたときに危険側に倒れるか、安全側に倒れるか。 選んだつもりがなくても、方式を決めた時点で決まっています。

設計判断② 知らない値が来たら、必ず弱い方に倒す

役割は X-User-Role というヘッダーで受け取っています。ここに来る値は、いつも想定どおりとは限りません。空のこともあれば、大文字違いのこともあり、そもそもヘッダーが無いこともあります。

このとき「知らない値だから admin にしておく」という作りにすると、こうなります。

攻撃する側がやることは、ヘッダーを送らないことだけです。

何も推測せず、何も破らず、1行送らなかっただけで管理者になれます。「何もしない」が最強の攻撃になる設計です。

なので、知らない値は必ず viewer(最小権限)に倒しています。何を送っても、何を送らなくても、弱い権限にしかなりません。

ここまで書いて気づいたのですが、設計判断①と②は同じことを言っています。

①は「書き忘れたとき」、②は「知らない値が来たとき」。どちらも想定外が起きたときに、どちらへ倒れるかの話でした。

想定内のことを正しく処理するのは当たり前です。設計の質が出るのは、想定外が起きたときにどちらへ倒れるかを、あらかじめ決めてあるかどうかだと思っています。

設計判断③ 検査は「安い順」に並べる

リクエストが来たとき、処理の順番はこうしています。

① レート制限(カウンターを見るだけ)
② 認証(鍵の照合。暗号の計算が走る)
③ サーバーの組み立て(一番重い)

逆にすると何が起きるか。認証を先にした状態で、間違った鍵で10万回叩かれたとします。10万回ぶんの暗号計算をしてから、10万回とも弾くことになります。

弾く作業そのもので潰れます。守るために入れた認証が、そのまま攻撃の道具になる。

レート制限を先に置けば、61回目からはカウンターを見るだけで断れます。暗号計算まで到達しません。

正しい処理を入れていても、順番を間違えると逆効果になる。 これは認証に限った話ではないと思います。


弱点と限界

ここまで書いてきた設計にも、成立する条件と、割り切った部分があります。隠さずに書いておきます。

1. 役割をヘッダーで受け取っている

このサーバーは、X-User-Role というヘッダーの値を信じています。つまり——正しいトークンを持っている相手なら、ヘッダーを書き換えるだけで admin になれます。

これは想定どおりの動作です。この設計は、信頼できる呼び出し元(Bot やゲートウェイ)の後ろに置く前提だからです。本人確認はその手前で終わっていて、このサーバーは「誰として扱うか」だけを受け取ります。

役割の判定を各サービスで二重に実装すると、判定がずれたときに気づけません。判定は一箇所に集め、後段はその結果を受け取る、という分担にしています。

なので、このサーバーをインターネットに直接晒すと、そのまま脆弱性になります。 その場合は、役割をヘッダーではなくトークン自体(署名付きトークンなど)から導出するように書き換えてください。

2. レート制限がプロセスのメモリにしかない

サーバーレスや複数インスタンスで動かすと、実行のたびに別のメモリになりうるので、全体としては上限を超えて通ることがあります。厳密に効かせたい場合は Redis 等の外部ストアに差し替えてください。差し替える場所は1ファイルに閉じてあります。

3. 固定ウィンドウ方式なので、境界で最大2倍通る

59秒目に60回、61秒目に60回で、2秒間に120回通ります。厳密にしたいならスライディングウィンドウ方式にする必要があります。

雛形の目的は「認証とロール制御の型を示すこと」なので、ここは意図的に最小限にしています。


まとめ

MCPサーバーを作ること自体は、そんなに難しくありません。公式のサンプルどおりにやれば動きます。

難しいのは、それを社内で承認できる状態にすることでした。

この記事で書いた3つは、結局どれも同じことを言っています。

  • 書き忘れたとき → その道具は誰にも見えなくなる
  • 知らない値が来たとき → 一番弱い権限になる
  • 大量に叩かれたとき → 一番安い検査で先に断る

想定外のことが起きたとき、どちらに倒れるか。 決めてあるかどうか、それだけです。

特に2つ目。知らない値が来たときに admin にしない。 一行で済む話ですが、逆にすると、ヘッダーを送らないだけで管理者になれるサーバーができあがります。動いてしまうので、誰も気づきません。

コードはMITライセンスで公開しています。そのまま使ってもらって構いません。

https://github.com/kazuhiro2188-lgtm/mcp-role-server

Discussion