安全
社区译文;来源: 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)阻止其他网页驱动后端,但不带Originheader 的请求(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 的命令会显式失败。它绝不会替换那个守护进程。
它在内存中持有什么
在其运行期间,守护进程会为每个打开的连接持有:
- 活动的 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.sock | 0600 | IPC socket(仅 Unix)。 |
mcpdod.lock | 0600 | 单实例锁,持有守护进程的 pid。 |
mcpdod.token | 0600 | IPC bearer token。启动时写入,正常关闭时移除。 |
mcpdod.log | 0600 | 守护进程的 stderr,每次启动时重建。启动失败和机密存储警告会落在这里。 |
守护进程通过与其他客户端相同的 oauth.json 和机密存储写入 OAuth 状态与机密,因此机密存储
项下的所有内容同样适用于它。