安全

社区译文;来源: MCP 80041ab0

Inspector 是一个开发者工具,它持有真实凭据并启动真实进程。了解它的保护范围、信任假设和安全边界,有助于判断哪种部署方式适合你的机器。配置页和配方页会链接到这里,而不是重复这些论证。

简要版本:

  • web 后端可以启动进程。 它的 API token 就是横在该能力与任何其他能访问该端口的东西之间的那道墙。
  • 有可用钥匙串时,机密会进入操作系统钥匙串。 没有钥匙串时会落到文件里,该文件除非你提供密钥否则就是明文,而这种回退是自动发生的。
  • stdio 服务器以你的身份运行。 Inspector 能读到的任何东西,它启动的服务器通常也能读到。
  • mcpdo 守护进程是一个长期存活、持有活动连接的进程,由 token 和同用户文件权限守护。

Web 后端及其 API token

web 客户端是一个浏览器应用,其后端是拥有 MCP 连接的 Node 服务器。该服务器可以按请求启动 stdio 进程,因此每个 /api/* 路由都要求每次启动独立的 bearer token(x-mcp-remote-auth: Bearer <token>)。浏览器如何获得它见会话 token 。

token 保护什么、不保护什么:

  • 对于非浏览器客户端,token 才是真正的守卫。 来源允许列表(ALLOWED_ORIGINS)阻止其他网页驱动后端,但不带 Origin header 的请求(curl、脚本、任何非浏览器客户端)会完全跳过该检查。
  • GET / 会披露该 token。 后端把它注入所服务的 HTML,以便刷新或书签仍能工作。因此任何能加载该页面的人都能读到 token,这也是为什么绑定地址比 token 的值更重要。固定你自己的 MCP_INSPECTOR_API_TOKEN 并不能改变这一点,因为自定义 token 与生成的 token 一样会被披露。
  • DANGEROUSLY_OMIT_AUTH=true 会完全移除这道守卫。 此后任何能访问该端口的东西都能以你的身份启动进程,并使用 Inspector 持有的任何 OAuth token。只有 true 或 1 会关闭身份验证;其他任何值(包括 false)都会保持开启。
  • 只有 /api/* 受门禁保护。 页面本身、其静态资源和 GET /healthz 无需 token 即可访问。/healthz 只返回 {"status":"ok"},供容器与编排器探针使用。

后端监听的地址

后端默认绑定 127.0.0.1。除非设置 DANGEROUSLY_BIND_ALL_INTERFACES=true,绑定所有接口(0.0.0.0、:: 及等价写法)都会被拒绝。绑定某一个特定地址无需该选择加入,因为那是一次有意的暴露,而不是一次暴露全部。参见在网络中托管 。

绝不要把 DANGEROUSLY_OMIT_AUTH 与非 loopback 绑定组合使用。如果你需要让其他人访问 Inspector,请在其前面放一道真正的访问控制边界:一个带身份验证的反向代理、一条 SSH 隧道,或一个私有网络。

Docker

Docker 镜像 会改变上述图景中的两件事。

只在 loopback 上发布端口

在容器内部,Inspector 必须绑定 0.0.0.0 才能通过 -p 访问,因此镜像设置了 DANGEROUSLY_BIND_ALL_INTERFACES=true。该选择加入管辖的是容器的接口,而不是主机的。哪些主机接口能看到 Inspector 取决于你如何发布端口:

发布 flag可从何处访问
-p 127.0.0.1:6274:6274仅本机。
-p 6274:6274(裸写)每个主机接口,即你的整个网络。

裸写 -p 会把一个会启动进程的后端,以及披露其 token 的页面,放到你的局域网上。每个发布的端口都要保留 127.0.0.1: 前缀(6274,以及若你发布 MCP Apps 监听器时的 6275 / 6278)。

数据卷把机密放到磁盘上

容器没有操作系统钥匙串。若 /home/node/.mcp-inspector 上没有卷,机密会在会话期间留在内存中,容器退出时丢失。挂载该卷以保留服务器列表,同时也把机密切换为卷上的 secrets.json 文件,而该文件除非你提供密钥否则就是明文。此后 root 以及主机 docker 组的每个成员(等价于 root)都能读取它,任何取得该卷备份、快照或副本的人也能读取。

请以文件方式通过 MCP_INSPECTOR_SECRET_KEY_FILE(Docker 或 Compose secret)提供密钥,而不是作为环境变量:用 -e MCP_INSPECTOR_SECRET_KEY=… 传入的密钥对任何能运行 docker inspect 或 docker exec 的人可见。配方页给出了这两种形式。

机密存储

Inspector 把凭据排除在 mcp.json、client.json 和 oauth.json 之外,这样共享、提交或同步这些文件时不会泄露它们。以下内容作为机密存储:

  • 已获取的 OAuth token(access、refresh 和 ID token),受 MCP_INSPECTOR_PERSIST_TOKENS 约束,以及企业托管授权产生的 IdP 会话 token;
  • 每个服务器的 OAuth 客户端 secret,以及企业 IdP 客户端 secret;
  • 动态注册的客户端 secret 及其注册访问 token;
  • 每个 stdio 服务器的 env: 值。

headers 不是机密。 它们会按写入原样保存在 mcp.json 中,因此携带凭据的 header(API key、静态 Authorization 值)会留在文件里。

存储如何被选择以及如何更改,见机密存储于何处 。本节讨论风险。

自动的明文回退

在无法访问操作系统钥匙串的主机上(没有 libsecret 或未运行 Secret Service 的 Linux、没有 D-Bus 会话的无头服务器或 SSH 会话、Android/Termux),Inspector 会自动把机密存放在 ~/.mcp-inspector/secrets.json。你并未主动要求这样做,而除非你提供密钥,该文件是未加密的。

唯一的迹象是选择存储时 stderr 上的一条警告,以及 web 客户端设置对话框中的页脚。

请把这当作风险,而不只是一个配置事实。文件以 0600 权限写入,这只挡得住其他非 root 用户,别的一概挡不住。要消除它,请做以下之一:

  • 恢复钥匙串(安装 libsecret 并运行 Secret Service,例如 gnome-keyring,或在桌面会话内运行);
  • 用 MCP_INSPECTOR_SECRET_KEY_FILE 中生成的密钥加密该文件;
  • 或设置 MCP_INSPECTOR_SECRET_STORE=memory,并在每个会话中重新输入机密。

文件存储防范哪些风险

文件存储是为没有钥匙串的机器而存在的,它比钥匙串弱。请把在其中保存机密(即使已加密)视为中等风险。

没有密钥(明文,权限 0600):

威胁是否防护?
机器上的其他非 root 用户(在权限生效期间)是
root,以及在容器主机上 docker 组的每个成员否
任何拿到该文件副本的人:备份、快照、已同步的主目录、一次提交否
任何以你的用户身份运行的程序,包括 Inspector 启动的 stdio 服务器否

有密钥(AES-256-GCM,使用 scrypt 和每次写入时生成的随机盐派生密钥):

威胁是否防护?
文件单独泄露(备份、快照、副本或提交),前提是密钥高熵且未随之泄露是
任何能读取密钥所在位置的人否
主机上的 root,或 docker 组:它们能读取文件、读取密钥,或读取持有解密值的进程内存否
以同一用户身份运行的代码否
弱口令:任何持有该文件的人都可以快速离线猜测,因为每次保存的派生为速度而保持较低 scrypt 成本否

密钥所在位置决定了第二行。使用 MCP_INSPECTOR_SECRET_KEY 时,密钥位于 Inspector 的环境中,同一用户或 root 可通过 /proc/<pid>/environ 读到它,容器中可通过 docker inspect / docker exec 读到,也可从你存放它用于启动的任何位置(shell profile、.env 文件、Compose 文件)读到。MCP_INSPECTOR_SECRET_KEY_FILE 将其缩小到能读取该密钥文件的人,但 Inspector 必须读取它,因此同一用户也能读取。如果密钥与机密文件放在一起——同一备份、同一卷或同一仓库——加密就毫无收益。

简言之,加密把"文件泄露了"变成"文件和密钥都泄露了"。它无法防范已经在机器上或容器中拥有 root,或拥有 Inspector 自身用户身份的人。当这不可接受时,请使用钥匙串或内存存储。

有两种失败模式被故意做得显眼而不是静默:

  • 如果密钥文件缺失、不可读或为空,如果 MCP_INSPECTOR_SECRET_KEY_FILE 被设为空值,或两个密钥变量都被设置,存储会拒绝读写,而不是回退到明文。
  • 如果口令变更或丢失,现有文件将无法再解密。Inspector 会把它读作空,并拒绝覆盖它,因此请恢复口令,或删除该文件并重新输入这些值。

stdio 服务器以你的身份运行

stdio MCP 服务器是 Inspector 以你的用户权限启动的进程,与任何 MCP 宿主完全一样。

  • 环境: Inspector 不会把自己的环境透传下去。stdio 服务器只会得到一个简短的白名单(macOS 和 Linux 上的 HOME、LOGNAME、PATH、SHELL、TERM、USER)加上它配置的 env:。因此 MCP_INSPECTOR_SECRET_KEY 中的密钥不会被直接交给它。
  • 但它属于同一用户。 服务器可以自己打开 secrets.json、读取 oauth.json 和你的 catalog,并且通常能通过 /proc/<pid>/environ 读到 Inspector 的环境。环境白名单是卫生措施,不是隔离。

只连接那些你愿意把这些机密托付给它的 stdio 服务器。要隔离一个你不信任的服务器,请把它的命令包进容器,例如用 docker run -i --rm --network none <image> 作为 stdio 命令。HTTP 和 SSE 服务器不运行本地代码,因此不需要进程隔离。

mcpdo 连接守护进程

mcpdo 通过把连接交给后台守护进程 mcpdod,在命令之间保持连接打开。它会在第一条需要它的命令(mcpdo connect,或任何针对某个连接的命令)时自动启动。没有单独的"启动守护进程"步骤需要选择加入。本节说明该进程暴露了什么。

客户端如何访问它,以及还有谁能访问

平台端点
macOS / Linux守护进程目录(默认 ~/.mcp-inspector)内的 Unix socket,名为 mcpdod.sock。
Windows由守护进程目录派生的具名管道 \\.\pipe\mcp-conn-<hash>。其周围没有目录权限,因此 token 才是守卫。

这里没有 TCP 端口,因此机器之外无法访问该守护进程。在 Unix 上,守护进程目录会被创建(若已存在则收紧)为 0700、归你所有,且必须是真实目录而非符号链接。其中的 socket 和锁文件为 0600。因此其他非 root 用户无法访问该 socket。root 以及任何以你的身份运行的进程可以。

目录按以下顺序选择:MCP_INSPECTOR_DAEMON_DIR,然后是 MCP_STORAGE_DIR,然后是 ~/.mcp-inspector。超过平台限制的 socket 路径(macOS 上约 104 字节,Linux 上 108 字节)会被直接拒绝,并报错指出应缩短哪个变量。

它如何对命令进行身份验证

每个请求都必须携带 bearer token。不存在未经身份验证的请求路径。

  • 共享模式(默认): 启动守护进程的那条 mcpdo 命令会生成一个随机的 256 位 token,并在其环境中传给守护进程。守护进程将其发布到守护进程目录中的 mcpdod.token(权限 0600),以便同一用户运行的任何 mcpdo 命令都能读到。该文件上的文件系统权限就是信任边界,与 socket 具有相同的同用户边界。
  • 私有模式: eval "$(mcpdo private)" 会在 $TMPDIR/mcp-conn-<uid>/ 下创建一个全新的 0700 目录,并把 MCP_INSPECTOR_DAEMON_DIR 和 MCP_INSPECTOR_DAEMON_TOKEN 导出到该 shell,使该 shell 获得自己的守护进程和自己的连接。使用前会检查父目录 mcp-conn-<uid> 的所有权和符号链接,因为 $TMPDIR 可能是共享的。

token 以常量时间比较。超过 1 MiB 的请求行会被拒绝。向活动守护进程出示错误 token 的命令会显式失败。它绝不会替换那个守护进程。

私有模式在 shell 之间隔离连接和守护进程状态。它不是针对以你的用户身份运行的其他进程的安全边界:任何拥有你 UID 且得知守护进程目录的东西都能读到其 token。如需硬边界,请使用独立用户账号或容器。

它在内存中持有什么

在其运行期间,守护进程会为每个打开的连接持有:

  • 活动的 MCP 连接,对 stdio 服务器还包括以守护进程为父进程启动的子进程;
  • 该连接的解析后配置,包括从机密存储取出的 stdio env: 值;
  • HTTP 连接正在使用的 OAuth token,它还会从存储重新读取这些 token 以重新建立中断的传输;
  • 非交互命令留下挂起的任何信息征询,直到被回答或在 10 分钟后过期。

守护进程以启动它的那条 mcpdo 命令的环境启动。它继承该 shell 的变量(包括在其中设置的 MCP_INSPECTOR_SECRET_KEY,以及它自己的 IPC token MCP_INSPECTOR_DAEMON_TOKEN),并在其整个生命周期内保持这些变量,即使你在 shell 中修改了它们。它启动的 stdio 服务器仍只收到上述白名单加上它们的 env:,这些值是从运行 mcpdo connect 的那个 shell 快照而来。

在没有钥匙串的主机上,mcpdo 绝不把内存存储作为自动回退。它的前端命令与守护进程是不同进程,因此按进程的存储无法把 token 从一个传到另一个。mcpdo 改用共享的 secrets.json 文件,并带有与上述相同的"除非提供密钥否则明文"警告。显式设置的 MCP_INSPECTOR_SECRET_STORE=memory 仍然优先。mcpdo 每次 connect 打印一次存储警告,而不是在每条命令上都打印。

生命周期

  • 启动: 在第一条需要它的命令时。如果两个同时启动,O_EXCL 锁(mcpdod.lock)会让恰好一个胜出。已死守护进程留下的锁会被回收,活动守护进程绝不会被接管。
  • 停止: mcpdo daemon stop,或 SIGINT / SIGTERM。它也会在最后一个连接关闭约 60 秒后自行退出。没有最长生命周期:只要还有任何连接打开,守护进程就会保持运行。
  • 正常停止时: 拒绝新工作,进行中的请求获得短暂宽限期,每个连接被关闭,socket、token 文件和锁被移除。
  • 崩溃时: 它持有的每个连接都会消失,stdio 服务器会失去其 stdin,这通常会终止它们。已存储的 OAuth token 和机密不受影响。下一条 mcpdo 命令会启动新的守护进程,它会清理陈旧 socket、回收锁并写入新 token。连接必须用 mcpdo connect 重新建立。

用 pgrep mcpdod 查找游离的守护进程。它会把进程标题设为 mcpdod。

它向磁盘写入什么

全部位于守护进程目录中,该目录为 0700:

文件权限内容
mcpdod.sock0600IPC socket(仅 Unix)。
mcpdod.lock0600单实例锁,持有守护进程的 pid。
mcpdod.token0600IPC bearer token。启动时写入,正常关闭时移除。
mcpdod.log0600守护进程的 stderr,每次启动时重建。启动失败和机密存储警告会落在这里。

守护进程通过与其他客户端相同的 oauth.json 和机密存储写入 OAuth 状态与机密,因此机密存储 项下的所有内容同样适用于它。