MCP 网关可以完成身份认证、限流和请求审计,但它看不到全部风险:工具可能在网关放行后执行危险命令,连接器可能把数据发送到不受信任的地址,模型也可能被提示注入诱导,以合法身份完成不合法的操作。
生产级 MCP 安全因此不能只依赖入口。更稳健的思路是纵深防御,并在每类风险最早出现、且仍可被可靠判断的位置实施控制。可以把这套架构拆成四层:安全执行、管理基础设施、出站信任和语义完整性。
网关之后仍然存在什么风险
网关擅长判断“谁可以调用哪个 MCP 服务”,却很难独自回答下面这些问题:
filesystem.write_file的目标路径是否越过工作目录?shell.run是否启动了未授权程序,或者通过参数绕过命令白名单?- 一个原本可信的 HTTP 工具是否在重定向后访问了内网元数据地址?
- 模型调用
send_email时,收件人和附件是否符合当前任务意图? - MCP Server 的工具描述是否在部署后被替换或篡改?
这些问题发生在不同边界上。把所有判断塞进网关,会迫使网关理解文件系统、进程、网络、凭据和自然语言意图,最终形成一个庞大但仍有盲区的策略点。
“最早可信控制点”是更实用的原则:文件路径应在打开文件前检查,出站地址应在建立连接前检查,高风险工具调用应在执行前结合参数和上下文判断。控制越接近实际动作,越不容易被编码、重定向或协议转换绕过。
四层控制如何分工
1. 安全执行:限制工具真正能做什么
这一层围绕 MCP Server 及其工具运行时展开。关键措施包括:
- 以非 root 用户运行工具,并使用只读根文件系统。
- 每个 Server 使用独立容器、沙箱或受限进程,避免共享宿主机权限。
- 只挂载必要目录,并明确区分只读与可写路径。
- 对命令、文件路径、参数大小、执行时间和输出大小设置硬限制。
- 不把云平台长期凭据、SSH 密钥或整个用户主目录注入工具环境。
参数校验不能只检查字符串前缀。例如,检查路径是否以 /workspace 开头,挡不住符号链接和 ../。执行层应先解析规范路径,再确认它仍位于允许目录中。
2. 管理基础设施:控制身份、配置和生命周期
MCP Server 不应被当作随意启动的本地插件。进入生产后,它需要具备普通服务应有的管理能力:
- 工作负载身份与短期凭据,而不是共享 API Key。
- Server、工具清单和策略配置的版本管理与审批。
- 部署产物签名、镜像摘要固定和依赖来源验证。
- 集中审计日志,并记录主体、会话、工具、参数摘要、策略结果和执行结果。
- 可快速撤销单个工具、Server 或身份权限的开关。
审计日志应避免直接保存密码、令牌和完整敏感文档。更合适的做法是记录参数类型、目标资源、内容哈希和脱敏摘要,同时保留关联请求所需的 trace ID。
3. 出站信任:不要默认相信工具访问的目标
很多 MCP 工具的价值来自外部连接,例如访问 Git 仓库、工单系统、数据库或 SaaS API。也正因为如此,出站流量是数据泄露、SSRF 和凭据滥用的重要通道。
出站控制应同时覆盖 DNS、IP、端口、TLS 身份和应用层目标:
- 默认拒绝外连,只允许业务必需的域名或服务身份。
- 阻止环回地址、链路本地地址、云元数据地址和未授权内网网段。
- 限制自动重定向,并对每次重定向后的目标重新执行策略。
- 使用短期、按目标绑定的凭据,避免一个令牌可访问多个系统。
- 通过受控 egress proxy 统一记录目标、响应码和传输字节数。
仅配置域名白名单还不够。DNS 重绑定、代理行为和服务端重定向都可能改变最终连接目标,因此网络层和应用层需要共同执行限制。
4. 语义完整性:判断“允许的调用”是否符合任务意图
语义层处理的是传统访问控制不擅长的问题:调用者有权限,工具本身也合法,但这次组合起来是否合理。
例如,读取一个公开工单后发送摘要可能是正常流程;读取凭据文件,再把内容发到工单评论,则应被阻止。可以围绕以下信号建立策略:
- 工具调用链是否出现异常组合。
- 参数中的目标用户、仓库、域名或数据分类是否超出任务范围。
- 工具描述、输入 schema 或 Server 身份是否与批准版本一致。
- 高风险动作是否经过确定性的确认或人工审批。
- 来自网页、文档和邮件的内容是否被错误地当成系统指令。
不要让另一个模型成为唯一的语义安全裁判。模型分类适合补充风险评分,但转账、删除、发布、发送外部邮件等动作仍应由确定性规则和审批机制兜底。
可以这样实践:执行前策略检查
下面是一个可直接运行的最小 Python 示例。它不依赖特定 MCP SDK,假设 MCP Host 在调用工具前把工具名和参数交给 authorize。实际接入时,可把同样的检查放进 Host 中间件、Server 包装器或独立策略服务。
将代码保存为 policy_check.py,然后运行 python policy_check.py:
from pathlib import Path
from urllib.parse import urlparse
import ipaddress
import socket
WORKSPACE = Path("/tmp/mcp-workspace").resolve()
ALLOWED_HOSTS = {"api.github.com", "tickets.example.com"}
ALLOWED_TOOLS = {"read_file", "write_file", "http_get"}
def path_is_allowed(raw_path: str) -> bool:
candidate = Path(raw_path).expanduser().resolve(strict=False)
return candidate == WORKSPACE or WORKSPACE in candidate.parents
def host_is_public(hostname: str) -> bool:
try:
addresses = socket.getaddrinfo(hostname, None)
except socket.gaierror:
return False
for address in addresses:
ip = ipaddress.ip_address(address[4][0])
if not ip.is_global:
return False
return True
def authorize(tool: str, arguments: dict) -> tuple[bool, str]:
if tool not in ALLOWED_TOOLS:
return False, "tool is not allowlisted"
if tool in {"read_file", "write_file"}:
if not path_is_allowed(str(arguments.get("path", ""))):
return False, "path escapes the workspace"
if tool == "http_get":
parsed = urlparse(str(arguments.get("url", "")))
if parsed.scheme != "https" or parsed.hostname not in ALLOWED_HOSTS:
return False, "outbound destination is not approved"
if not host_is_public(parsed.hostname):
return False, "destination resolves to a non-public address"
return True, "allowed"
if __name__ == "__main__":
tests = [
("read_file", {"path": "/tmp/mcp-workspace/report.txt"}),
("read_file", {"path": "/etc/passwd"}),
("http_get", {"url": "https://api.github.com/repos/example/demo"}),
("http_get", {"url": "http://169.254.169.254/latest/meta-data/"}),
("shell", {"command": "curl https://unknown.example"}),
]
for tool, arguments in tests:
allowed, reason = authorize(tool, arguments)
print(f"{tool:10} allowed={allowed:<5} reason={reason}")
这个示例展示了控制点的位置,但生产实现还需要补足几个边界:
- 在真正建立连接的组件中再次校验解析后的 IP,降低 DNS 重绑定风险。
- 禁止或逐跳验证 HTTP 重定向。
- 写文件时使用避免符号链接竞态的安全打开方式。
- 将策略决定、主体身份和 trace ID 写入审计系统。
- 给策略查询设置超时,并明确超时后默认拒绝还是进入受限降级模式。
Kubernetes 中把出站默认值设为拒绝
如果 MCP Server 运行在 Kubernetes,可以先用 NetworkPolicy 建立默认拒绝基线。下面的 YAML 假设集群网络插件支持 NetworkPolicy,并且 DNS 位于带有 k8s-app: kube-dns 标签的命名空间和 Pod 中;部署前需要按实际集群标签调整。
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: mcp-default-deny-egress
namespace: mcp
spec:
podSelector:
matchLabels:
app.kubernetes.io/part-of: mcp
policyTypes:
- Egress
egress: []
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: mcp-allow-dns
namespace: mcp
spec:
podSelector:
matchLabels:
app.kubernetes.io/part-of: mcp
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
应用并检查策略:
kubectl apply -f mcp-network-policy.yaml
kubectl -n mcp get networkpolicy
kubectl -n mcp describe networkpolicy mcp-default-deny-egress
标准 NetworkPolicy 主要按 IP 和端口工作,不适合直接表达稳定的域名白名单。生产环境通常还需要 egress gateway、服务网格、CNI 扩展能力或显式代理来验证域名与 TLS 身份。
落地顺序:先收窄爆炸半径,再增加语义判断
纵深防御不等于第一天部署所有复杂组件。可以按风险和可验证性逐步推进:
- 盘点所有 MCP Server、工具、凭据、可写目录和外部目标,删除无人负责的实例。
- 隔离执行环境,使用非 root、只读文件系统、最小挂载和资源上限。
- 将出站流量改为默认拒绝,只开放已确认的业务目标。
- 为每次工具调用记录身份、策略结果、目标和 trace ID,并对敏感参数脱敏。
- 对删除、发布、付款、外发和权限变更设置确定性规则与人工审批。
- 固定 Server 版本、工具 schema 和部署摘要,检测未经批准的变化。
- 用提示注入、路径穿越、SSRF、重定向和凭据泄露场景持续做回归测试。
网关仍然重要,但它应是多层控制中的入口,而不是唯一防线。真正可靠的生产架构会让每一层只回答自己最擅长的问题,并让高风险操作必须连续穿过身份、执行、网络和语义检查。即使某一层判断失误,其余层仍能限制影响范围并留下可调查的证据。