关于 opsgate
opsgate 是供 AI Agent 检查和管理基础设施的 MCP 服务,不向 Agent 提供通用 Shell。它通过类型化操作、策略模式、变更审批和防篡改审计日志控制主机命令的执行。
主要功能
- 01不使用 Shell 字符串的类型化运维工具
- 02observe、operate 和 full 策略模式
- 03变更操作需审批
- 04按主机和工具限制目标
- 05哈希链审计日志
- 06本地 stdio 与认证 HTTP 传输
适用场景
- 01检查服务、端口、进程和磁盘用量
- 02查看 Docker 容器与系统日志
- 03审批并审计服务或容器变更
MCP 相关
通过 stdio 或 HTTP 向 MCP 客户端提供类型化基础设施工具。变更操作可通过 MCP elicitation 请求人工审批;observe 模式为只读。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
docker run -i --rm ghcr.io/polymatx/opsgate:0.1.3本地服务需要在你的电脑上安装运行环境(如 Node.js 18+)。各客户端的配置文件位置不同,请参考客户端文档。
ghcr.io/polymatx/opsgate:0.1.3stdio工具
本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。
详细介绍
opsgate
AI Agent 与服务器之间的安全关卡。
一个 MCP 服务,让 Claude、Cursor 或任何 AI Agent 操作真实基础设施, 而无需给它 root shell。
看看它的样子 · 30 秒试用 · 亲自试着攻破它 · 工具 · 安全模型 · 参与贡献
看看它的样子
你提出问题。Agent 使用只读工具调查并给出答案。
你: 我的服务器上有东西在监听 8000 端口,但我不记得是什么。帮我查查。
▸ listening_ports(host: "hetzner")
Netid State Recv-Q Send-Q Local Address:Port Process
tcp LISTEN 0 511 0.0.0.0:443 users:(("nginx",pid=249619),…)
tcp LISTEN 0 511 0.0.0.0:80 users:(("nginx",pid=249619),…)
tcp LISTEN 0 4096 0.0.0.0:22 users:(("sshd",pid=237597),…)
tcp LISTEN 0 151 127.0.0.1:3306 users:(("mysqld",pid=237695),…)
tcp LISTEN 0 5 127.0.0.1:8000 users:(("python3",pid=237608),…)
▸ service_list(host: "hetzner", state: "running")
docker.service loaded active running Docker Application Container Engine
mysql.service loaded active running MySQL Community Server
nginx.service loaded active running A high performance web server…
php8.3-fpm.service loaded active running The PHP 8.3 FastCGI Process Manager
pm2-root.service loaded active running PM2 process manager
Claude: 8000 端口上运行着一个普通的
python3进程(pid 237608),仅绑定到 localhost, 因此外部无法访问。它不受 systemd 管理——没有对应的单元——这意味着重启后它不会继续运行。 最有可能是有人在 shell 中手动启动的。
现在试试会改变状态的操作。在 operate 模式下,Agent 不能直接执行:
你: 好,nginx 配置改好了。重新加载它。
▸ nginx_test(host: "hetzner")
nginx: configuration file /etc/nginx/nginx.conf test is successful
▸ nginx_reload(host: "hetzner")
┌─ opsgate: approval required ─────────────────────────────────┐
│ allow nginx_reload on host "hetzner"? │
│ │
│ The nginx configuration passed validation. │
│ This will reload nginx. │
│ │
│ Command: 'nginx' '-s' 'reload' │
│ [ Approve ] [ Deny ] │
└─────────────────────────────────────────────────────────────┘
你点击 Deny,什么也不会发生——Agent 会收到操作员拒绝了请求的通知。点击 Approve,操作就会执行, 并记录到审计日志中。无论哪种情况,你都能先看到确切的命令。
(上方的工具输出来自真实服务器;提示词由你的 MCP 客户端呈现,因此显示方式取决于该客户端。)
30 秒试用(无需服务器)
opsgate 无需任何配置文件即可运行——在本机以只读模式运行:
go install github.com/polymatx/opsgate/cmd/opsgate@latest
opsgate serve
opsgate v0.1.3 | mode=observe | default_host=local | audit=~/.opsgate/audit.jsonl
就这么简单。无需配置,无需 SSH,且默认处于 observe 模式——因此它可以读取你的磁盘使用情况和日志,
但会拒绝执行任何更改:
▸ disk_usage() → works
▸ service_restart(...) → Refused by opsgate policy: host is in observe mode
将客户端连接到它,然后询问“这台机器上什么东西占用了磁盘空间?”准备好连接真实主机后,运行
opsgate init,即可将初始配置写入 ~/.opsgate/config.yaml。
哪些客户端可以访问它
opsgate 在你的机器上(或服务器本身)运行,并持有你的 SSH 密钥,因此需要使用哪种传输方式, 取决于客户端实际运行的位置:
| 客户端 | 传输方式 | 原因 |
|---|---|---|
| Claude Code(终端) | stdio | 在你的机器上原生运行并启动二进制文件 |
| Claude Desktop、Cursor、VS Code | stdio | 同上——本地进程 |
| 云端或沙盒环境(网页聊天、基于 VM 的 Agent) | HTTP | 无法启动本地进程,甚至无法查看你的 SSH 密钥——只能访问 URL |
如果客户端提示它无法访问你的服务器,那它几乎肯定属于沙盒环境。请改用 HTTP, 而非 stdio。
在公开这个端点之前,请先阅读该部分的警告:它会在你将其暴露到的网络上提供基础设施管理 API。
将其接入客户端
Claude Code
作为插件:包含 MCP 服务、一个 server-ops 技能,用于教会 Agent 先诊断再行动,并将拒绝视为最终结果;还包含一个只读的 /opsgate:health-check [host] 命令。
/plugin marketplace add polymatx/opsgate
/plugin install opsgate@opsgate
该插件会运行 opsgate serve,后者读取 ~/.opsgate/config.yaml(opsgate init 会写入一个初始配置文件)。它绝不会读取你当前打开的项目中的配置。
也可以只添加 MCP 服务:
claude mcp add opsgate -- opsgate serve --config ~/.opsgate/config.yaml
Claude Desktop / Cursor(claude_desktop_config.json 或 .cursor/mcp.json)
{
"mcpServers": {
"opsgate": {
"command": "opsgate",
"args": ["serve", "--config", "/absolute/path/to/opsgate.yaml"]
}
}
}
Docker(ghcr.io/polymatx/opsgate)
适用于通过 SSH 访问的主机。在容器内,local 指容器本身。
docker run -i --rm \
-v ~/.opsgate:/root/.opsgate \
-v ~/.ssh:/root/.ssh:ro \
ghcr.io/polymatx/opsgate serve
该镜像也用于将 opsgate 列入 MCP Registry,名称为 io.github.polymatx/opsgate。
任意客户端,通过 HTTP 接入(云端/沙盒客户端需要此方式)
http:
addr: "127.0.0.1:8080"
auth_token: "a-long-random-token" # sent as: Authorization: Bearer <token>
opsgate serve --http 127.0.0.1:8080
最简洁的部署方式是在 opsgate 管理的服务器上运行 opsgate——这样完全不需要 SSH 密钥(default_host: local)——并置于 TLS 终止反向代理之后:
client ──HTTPS──▶ nginx / Caddy ──▶ 127.0.0.1:8080 opsgate
+ Bearer token
除非设置了 http.auth_token,否则 opsgate 拒绝在非回环地址上提供 HTTP 服务。
警告
暴露此端点意味着在该网络上提供基础设施管理 API,而 Bearer 令牌将成为唯一的保护措施。如果必须公开:请保持
mode: observe,为令牌生成至少 32 字节的随机值,要求使用 TLS,并在代理处添加 IP 允许列表。将其绑定到 VPN 接口(WireGuard、Tailscale),而不是公开互联网,要安全得多——只有你自己的设备能访问,其他人都不能。另请注意,
operate模式需要有人类能够响应审批提示。无人值守的 HTTP 部署通常意味着使用observe。
此方案解决的问题
如今,要让 AI Agent 运行你的服务器,就意味着通过 SSH 给它 exec("<any string>")。
通常的缓解方法是维护一个拒绝列表,列出那些看起来危险的命令。
这行不通。面对拥有无限种表达方式的对手,拒绝列表只能靠猜:
rm -rf / → blocked
r''m -rf / → not blocked
echo cm0gLXJmIC8K | base64 -d | sh → not blocked
而且,“看起来危险”甚至不是难点。systemctl restart nginx 是一条再普通不过的命令,但在黑色星期五下午 3 点执行可能造成灾难性后果。
方案
opsgate 不会过滤字符串。它绝不会给 Agent 一个字符串让它填写。
Agent 会获得 25 个类型化工具——service_restart(name)、docker_logs(container)、journal_tail(unit, since, priority)——opsgate 会自行将命令构建为 argv 数组。没有可供注入的 shell,因为系统绝不会根据 Agent 的输入拼装 shell 命令字符串。
// What opsgate does — the name is one argv element, whatever it contains
argv := []string{"systemctl", "restart", in.Name}
// What it never does
argv := []string{"sh", "-c", "systemctl restart " + in.Name}
如果将 nginx; rm -rf / 作为服务名称发送,systemctl 会将其接收为单个字面参数,并因找不到对应单元而失败。不会执行任何其他操作。
之后,每次调用都会通过四道关卡:
flowchart LR
A[Agent calls a tool] --> B{Policy<br/>mode + allowlist}
B -->|deny| X[Refused<br/>+ audited]
B -->|allow| D[Build argv]
B -->|needs approval| C{Human}
C -->|deny| X
C -->|approve| D
D --> E[Execute]
E --> F[Audit<br/>hash-chained]
| 层级 | 作用 |
|---|---|
| 结构化工具 | 命令以 argv 形式构建,绝不通过字符串拼接。注入在设计上就不可能发生,而不是靠过滤来防范。 |
| 默认拒绝策略 | 提供三种模式——observe(只读)、operate(写入需审批)和 full。支持按主机覆盖策略,并为每种工具设置目标允许列表。 |
| 人工审批关卡 | 修改操作会暂停并征求你的同意,同时显示确切命令。需要客户端支持 MCP elicitation;如果客户端无法发起询问,则会拒绝调用,而不会执行。 |
| 防篡改审计 | 每次调用(包括每次拒绝)都会追加到采用哈希链的 JSONL 日志中。任何一行被编辑或删除都能被检测到。 |
默认模式为 observe。开箱即用时,Agent 可以诊断你的服务器,但不会做出任何更改。
亲自试试
别轻信这些说法。以 observe 模式启动 opsgate,并设置
files.allow_paths: [/var/log],然后尝试以下操作——每一项都会被拒绝,并记录拒绝信息:
| 尝试操作 | 结果 |
|---|---|
service_status(name: "nginx; touch /tmp/pwned") | 拒绝——目标名称包含无效字符 |
file_read(path: "/var/log/../../etc/shadow") | 拒绝——路径不在允许的路径范围内 |
file_read(path: "/root/.ssh/id_rsa") | 拒绝——路径不在允许的路径范围内 |
file_grep(pattern: "x$(touch /tmp/pwned)") | 按字面字符串搜索;不会执行任何内容 |
service_restart(name: "nginx") | 拒绝——observe 模式不允许修改 |
设置了 allow_targets 时调用 journal_tail() | 拒绝——省略单元名称会读取整个日志 |
shell_exec(command: "id") | 拒绝——除非选择启用,否则根本不会注册此工具 |
然后确认没有任何内容被写入:
$ ls /tmp/pwned
ls: cannot access '/tmp/pwned': No such file or directory
$ opsgate audit verify
audit chain OK: 6 records verified in ~/.opsgate/audit.jsonl
不是七项,而是六项:禁用时,shell_exec 根本不是已注册工具,因此调用会在到达安全门之前就被协议拒绝。
如果你发现了确实能够通过的操作?那就是安全漏洞——请报告。
模式
mode: observe # read-only. Mutating tools are refused outright.
mode: operate # reads run freely; every write asks you first. ← recommended
mode: full # writes run unprompted. Still fully audited.
对生产环境的限制要比其他所有环境都严格:
mode: operate
hosts:
prod:
addr: 10.0.0.1
mode: observe # prod is read-only even though the default is operate
staging:
addr: 10.0.0.2 # inherits operate
限制工具可以操作的对象:
tools:
service_restart:
allow_targets: ["nginx", "myapp*"] # postgresql is not restartable, period
service_stop:
enabled: false # remove the tool entirely
shell_exec:
enabled: true # opt in to freeform shell (still approval-gated)
设置 allow_targets 也会让目标成为该工具的必填项:省略目标通常会扩大操作范围,超过任何允许的值——例如,不指定单元名称的 journal_tail 会读取整个日志——因此,无约束的调用会被拒绝,而不是默默放行。
Docker 工具只接受完全匹配的容器名称,不接受 ID 或 ID 前缀,因此像 db* 这样的模式只会匹配你在 docker ps 中看到的内容,不会匹配其他内容。files.allow_paths: [] 会完全关闭文件访问;省略此键则保留默认值。
所有选项请参阅 configs/opsgate.example.yaml。
工具
观察——始终可用
system_info · disk_usage · process_top · listening_ports · service_list ·
service_status · docker_ps · docker_logs · docker_inspect · docker_stats ·
compose_ps · journal_tail · journal_errors · file_read · dir_list ·
file_grep · nginx_test
修改——在 observe 模式下会被拒绝,在 operate 模式下需要经过审批
service_restart · service_start · service_stop · service_reload ·
docker_restart · docker_start · docker_stop · nginx_reload
选择启用
shell_exec — 任意 Shell 命令。除非启用,否则处于禁用状态;始终需要经过审批。它是为确实难以涵盖的少数需求而设,上面列出的工具则是为了让你很少需要用到它。
nginx_reload 会先运行 nginx -t,如果配置无法解析,就会拒绝重新加载。
值得向你的 Agent 提出的问题
- “为什么 web1 上的 nginx 会返回 502?” — 关联
service_status、journal_tail、docker_ps - “是什么占满了 prod 的磁盘?” — 先运行
disk_usage,再检查允许路径下的dir_list - “昨晚有没有发生崩溃?” — 检查各主机上的
journal_errors - “哪些容器处于不健康状态,分别重启了多少次?” — 使用
docker_inspect - “比较 staging 和 prod:哪些服务只在其中一个环境运行?” — 在两个环境中使用
service_list
审计日志
每次调用都会追加一行 JSON,其中包含上一条记录的哈希:
{"seq":41,"time":"2026-07-29T11:02:14Z","host":"web1","tool":"service_status","decision":"allow","exit_code":0,…,"prev_hash":"9f2c…","hash":"41ab…"}
{"seq":42,"time":"2026-07-29T11:02:31Z","host":"web1","tool":"file_read","args":{"path":"/root/.ssh/id_rsa"},"decision":"deny","error":"outside the allowed paths",…}
{"seq":43,"time":"2026-07-29T11:03:02Z","host":"web1","tool":"service_restart","phase":"intent","decision":"needs_approval","approved":true,…}
{"seq":44,"time":"2026-07-29T11:03:02Z","host":"web1","tool":"service_restart","phase":"outcome","decision":"needs_approval","approved":true,"exit_code":0,…}
拒绝操作也会记录——之后查到有人试图读取私钥且被拦截,正是你想要的结果。
会更改状态的调用会写入两条记录:在命令运行之前写入 phase: intent,之后写入
phase: outcome。如果无法写入意图记录,opsgate 就会拒绝执行——因此,已经发生的操作绝不会从日志中缺失。
$ opsgate audit verify
audit chain OK: 143 records verified in ~/.opsgate/audit.jsonl
编辑、删除或重新排列任何较早的行,都会导致验证在出错的确切位置失败:
$ opsgate audit verify
opsgate: audit chain INVALID after 41 records: line 42: record tampered (hash mismatch)
伪造字段并插入其中也行不通——验证会进行严格解码:
opsgate: audit chain INVALID after 2 records: line 3: json: unknown field "approved_by"
哈希链无法检测文件末尾被截断的记录,也无法检测具有写入权限的人从头重写日志的情况;如果这很重要,请将日志发送到设备外部或存入仅追加存储(参见威胁模型)。
名为 password 或 token 的参数会在写入前被删去敏感信息;其他所有内容,包括 shell_exec 命令,都会按发送时的原样记录,因此不要在工具参数中放入机密。超长值会被截断为短前缀,并附上其长度和 SHA-256。只存储命令输出的 SHA-256,绝不存储输出本身。
对比
还有其他通过 SSH 提供 MCP 的服务,它们也很有用,只是取舍不同。
| 自由格式 Shell MCP 服务 | opsgate | |
|---|---|---|
| Agent 发送的内容 | 命令字符串 | 固定工具的类型化参数 |
| 注入防护 | 对字符串使用正则表达式拒绝列表 | 不会构造 Shell 字符串——仅使用 argv |
| 审批 | 无,或由客户端自动批准 | 每次调用都由服务端审批,并显示确切命令 |
| 只读保证 | 依赖约定 | observe 模式不存在任何可执行变更的代码路径 |
| 审计 | 各不相同 | 哈希链式记录,包含拒绝操作,并在执行前记录意图 |
| 运行时 | 通常为 Node | 单个静态 Go 二进制文件,无需运行时 |
坦诚地说:opsgate 的工具集有限,因此你需要的功能可能尚未涵盖。这正是 shell_exec 和贡献的用武之地。
安全模型
opsgate 可以缩小影响范围。它不是沙箱,也不能让不可信的 Agent 安全地访问生产环境。
它能保证什么
- Agent 的输入永远不会变成 shell 语法。命令以
argv形式传递;SSH 传输会对每个元素使用单引号,因此每个参数都会保留为一个字面词。以-开头的目标名称会被拒绝,因此无法将其当作命令行选项。 observe模式无法执行修改操作。从只读模式到状态变更命令之间不存在任何代码路径。- 文件工具无法超出
files.allow_paths的范围——路径会在前缀检查前进行清理,因此/var/log/../../etc/shadow会被拒绝。 - SSH 主机密钥会根据
known_hosts验证。未知密钥会被拒绝,不会在首次使用时默认信任。 - 每项决策都会被审计,包括拒绝的操作;执行变更操作前也会先记录下来。
- 一次审批只针对一个提示和一次确切调用(主机、工具、命令),且只能使用一次;客户端无法预先批准从未询问过的调用。
- opsgate 只会从
--config或~/.opsgate/config.yaml读取配置,绝不会从启动目录读取,因此仓库无法附带会放宽策略的配置。
它无法保证什么
- 工具以你的 SSH 用户权限运行。如果该用户是 root,那么经批准的调用就会以 root 权限运行。请为 opsgate 配置一个权限最小化的专用用户。
- 启用并批准后,
shell_exec就能执行任意代码。这正是它的用途。 full模式会移除人工把关。只有在你接受这一点时才使用它。- 遭受提示注入的 Agent 仍可能使用看似合理的参数调用允许的工具。策略负责限制损害——请严格限制
allow_targets。 - 路径检查采用词法方式:允许目录中的符号链接若指向该目录之外,仍会被跟随。
- 审批是否可靠,取决于显示提示的 MCP 客户端是否诚实。会自行回答“接受”的客户端会使审批失效;请使用你信任的客户端。
有关威胁、控制措施和限制的详细信息,请参阅 docs/threat-model.md。
为什么要开发它
我管理自己的基础设施,也希望让 Claude 能访问它。所有现有方案都让我在“毫无用处”和“这是 root shell,祝你好运”之间做选择。opsgate 则取其中间路线:Agent 获得实际能力,我保留否决权和操作记录。
贡献
我们真诚欢迎贡献;该架构也让添加新工具变得简单——大多数只需约 20 行代码。请阅读 CONTRIBUTING.md,了解唯一重要的规则(命令必须使用 argv,绝不能拼接字符串)。
适合初次贡献的内容
- 更多结构化工具——Kubernetes(
kubectl get/describe/logs)、PostgreSQL 和 MySQL 状态、Caddy、Traefik、ufw/iptables状态、证书过期时间、apt/dnf待安装更新 - 策略原语——时间窗口(“工作时间内不重启”)、速率限制、每种工具的并发上限
- 审计接收端——syslog、仅追加的 S3、webhook
- 客户端兼容性报告——告诉我们审批提示在你的 MCP 客户端中的显示效果
go test -race ./...
go vet ./...
gofmt -l . # must print nothing
如果 opsgate 对你有用,点个 ⭐ 就能帮助其他管理自有基础设施的人找到它。
许可证
MIT——请参阅 LICENSE。
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。