opsgate について
opsgateは、汎用シェルを渡さずにAIエージェントがインフラを調査・管理できるMCPサーバーです。型付きツール、ポリシーモード、変更時の承認、改ざんを検知できる監査ログでホスト操作を制御します。
主な機能
- 01シェル文字列を使わない型付き運用ツール
- 02observe、operate、fullのポリシーモード
- 03変更操作の承認ゲート
- 04ホスト別・ツール別の対象制限
- 05ハッシュチェーン監査ログ
- 06ローカルstdioと認証付きHTTP
活用シーン
- 01サービス、ポート、プロセス、ディスク使用量の調査
- 02Dockerコンテナとシステムジャーナルの確認
- 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 エージェントとサーバーの間に設ける安全ゲートです。
Claude、Cursor、またはあらゆる AI エージェントが、root シェルを渡されることなく 実際のインフラを操作できる MCP サーバーです。
動作イメージ · 30 秒で試す · 自分で試す · ツール · セキュリティモデル · コントリビューション
動作イメージ
質問すると、エージェントは読み取り専用ツールで調査して回答します。
あなた: サーバーのポート 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 で動いているのは、localhost のみにバインドされた単独の
python3プロセス(pid 237608)です。そのため、外部からはアクセスできません。systemd による管理もなく、このプロセスを所有するユニットはありません。そのため、再起動後も動作し続けることはありません。おそらくシェルから手動で起動されたものです。
次は状態を変更する操作です。operate モードでは、エージェントが勝手に実行することはできません。
あなた: わかりました。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 ] │
└─────────────────────────────────────────────────────────────┘
「拒否」をクリックすると何も起こらず、エージェントにはオペレーターが拒否したことが伝えられます。「承認」をクリックすると操作が実行され、監査ログに記録されます。どちらの場合も、先に実行されるコマンドを正確に確認できます。
(上記のツール出力は実稼働中のサーバーから取得した実際の出力です。プロンプトの表示は 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 上で動作するエージェント) | HTTP | ローカルプロセスを起動できず、SSH キーも参照できません。アクセスできるのは URL のみです |
クライアントからサーバーにアクセスできないと報告された場合、ほぼ間違いなくサンドボックス環境で動作しています。stdio ではなく、HTTP で接続してください。
エンドポイントを公開する前に、そこに記載されている警告をお読みください。公開先のネットワーク上に、インフラ管理 API を設置することになります。
クライアントに接続する
Claude Code
プラグインとして使用できます。MCP サーバー、エージェントに行動する前に診断し、拒否を最終的なものとして扱うよう教える server-ops スキル、読み取り専用の /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 を管理対象のサーバー上で実行することです。そうすれば SSH キーは不要になり(default_host: local)、TLS 終端リバースプロキシの背後に配置できます。
client ──HTTPS──▶ nginx / Caddy ──▶ 127.0.0.1:8080 opsgate
+ Bearer token
http.auth_token が設定されていない場合、opsgate はループバック以外のアドレスで HTTP を提供しません。
警告
このエンドポイントを公開すると、そのネットワーク上でインフラ管理 API が利用可能になり、ベアラートークンだけが保護手段になります。公開せざるを得ない場合は、
mode: observeを維持し、32 バイト以上のランダムなトークンを生成し、TLS を必須にし、プロキシに IP 許可リストを追加してください。パブリックインターネットではなく VPN インターフェース(WireGuard、Tailscale)にバインドするほうが、はるかに安全です。自分のデバイスからは接続でき、ほかの人は接続できません。また、
operateモードでは、承認プロンプトに応答できる人間が必要です。通常、無人の HTTP デプロイではobserveを使用します。
この仕組みが解決する問題
現在、AI エージェントにサーバーを操作させるには、SSH 経由で exec("<any string>") を実行できる権限を与えることになります。
一般的な対策は、危険に見えるコマンドを拒否リストに登録することです。
これは機能しません。無限に言い換えができる攻撃者を相手にした拒否リストは、当てずっぽうにすぎません。
rm -rf / → blocked
r''m -rf / → not blocked
echo cm0gLXJmIC8K | base64 -d | sh → not blocked
しかも、「危険そうに見える」かどうかは、難しい問題ですらありません。systemctl restart nginx はごく普通のコマンドですが、ブラックフライデーの午後 3 時に実行すれば、壊滅的な結果になりかねません。
アプローチ
opsgate は文字列をフィルタリングしません。エージェントに入力用の文字列を渡すことは一切ありません。
エージェントに提供されるのは、型付きの 25 個のツールです。たとえば、service_restart(name)、docker_logs(container)、journal_tail(unit, since, priority) があります。opsgate はコマンドを argv 配列として自ら構築します。エージェントの入力からシェル文字列を組み立てることはないため、インジェクションに利用できるシェルはありません。
// 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 はそれを単一のリテラル引数として受け取り、不明なユニットとして失敗します。それ以外の処理は実行されません。
その後、すべての呼び出しは次の 4 つのゲートを通過します。
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 の 3 つのモードがあります。ホストごとのオーバーライドや、ツールごとの対象許可リストにも対応しています。 |
| 人間による承認ゲート | 変更を伴う呼び出しは一時停止して承認を求め、実行される正確なコマンドを表示します。MCP エリシテーションに対応したクライアントが必要です。承認を求められない場合、呼び出しは実行されず、拒否されます。 |
| 改ざん検知可能な監査ログ | 拒否を含むすべての呼び出しが、ハッシュチェーン付き JSONL ログに追記されます。行の編集や削除は検出できます。 |
デフォルトのモードは observe です。初期状態では、エージェントはサーバーを診断できますが、何も変更できません。
自分で試してみる
主張をうのみにしないでください。files.allow_paths: [/var/log] を設定して opsgate を
observe モードで起動し、以下を試してみてください。すべて拒否され、拒否の記録が残ります。
| 試す内容 | 結果 |
|---|---|
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
7つではなく6つです。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 — 任意のシェルコマンドを実行します。有効にしない限り無効で、常に承認が必要です。これは特殊なケースに対応するためのもので、上記のツールが揃っているため、使用する必要はほとんどありません。
nginx_reload は最初に nginx -t を実行し、構文を解析できない設定のリロードを拒否します。
エージェントに聞いてみるとよいこと
- 「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,…}
拒否した操作も記録されます。秘密鍵の読み取りをブロックした試みは、後から確認したいまさにその記録です。
状態を変更する呼び出しでは、2つのレコードが書き込まれます。コマンドの実行前に phase: intent が、実行後に phase: outcome が記録されます。intent レコードを書き込めない場合、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-over-MCP サーバーはほかにもあり、役立つものです。ただし、異なるトレードオフを選んでいます。
| フリーフォームのシェル MCP サーバー | opsgate | |
|---|---|---|
| エージェントが送信するもの | コマンド文字列 | 固定ツールへの型付き引数 |
| インジェクション対策 | 文字列に対する正規表現の拒否リスト | シェル文字列を構築せず、argv のみを使用 |
| 承認 | なし、またはクライアント側で自動承認 | 呼び出しごとにサーバー側で承認し、正確なコマンドを表示 |
| 読み取り専用の保証 | 慣例 | observe モードでは変更につながるコードパスがない |
| 監査 | さまざま | ハッシュチェーン方式で、拒否と実行前の intent を記録 |
| 実行環境 | 通常は Node | ランタイム不要の単一静的 Go バイナリ |
率直に言えば、opsgate のツールセットは有限であるため、必要なものがまだ対応されていない場合があります。そのための仕組みが shell_exec とコントリビューションです。
セキュリティモデル
opsgate は被害範囲を抑えます。サンドボックスではなく、信頼できないエージェントを本番環境に接続しても安全になるわけではありません。
保証すること
- エージェントの入力がシェル構文になることはありません。コマンドは
argvで渡され、SSH トランスポートは各要素をシングルクォートで囲むため、それぞれの引数は1つのリテラルな単語として扱われます。-で始まるターゲット名は拒否されるため、コマンドラインオプションとして使われることはありません。 observeモードでは変更を加えられません。読み取り専用モードから状態を変更するコマンドへ進むコードパスはありません。- ファイルツールが
files.allow_pathsの外に出ることはありません。パスはプレフィックスを確認する前に正規化されるため、/var/log/../../etc/shadowは拒否されます。 - SSH ホストキーは
known_hostsと照合して検証されます。不明なキーは拒否され、初回接続時に信頼されることはありません。 - 拒否を含むすべての判断が監査記録に残り、変更を伴うアクションは実行前に記録されます。
- 承認は、特定の1回の呼び出し(ホスト、ツール、コマンド)についての1つのプロンプトに対するもので、1回限りです。クライアントは、確認されていない呼び出しを事前承認できません。
- opsgate は
--configまたは~/.opsgate/config.yamlからのみ設定を読み込み、起動したディレクトリからは読み込みません。そのため、リポジトリにポリシーを緩める設定を含めることはできません。
保証しないこと
- ツールは SSH ユーザーの権限で実行されます。そのユーザーが root なら、承認された呼び出しも root 権限で実行されます。opsgate 専用の最小権限ユーザーを用意してください。
shell_execは、有効化して承認すると任意のコードを実行できます。それがこのツールの目的です。fullモードでは人間による確認がなくなります。それを受け入れられる場合に限り使用してください。- プロンプトインジェクションを受けたエージェントでも、もっともらしい引数で許可されたツールを呼び出すことはできます。被害を抑えるのはポリシーです。
allow_targetsは厳しく設定してください。 - パスの確認は字句的に行われます。許可されたディレクトリ内のシンボリックリンクが外部を指している場合、そのリンク先がたどられます。
- 承認の信頼性は、プロンプトを表示する MCP クライアントの誠実さに左右されます。独断で「承認」と回答するクライアントでは、この仕組みは機能しません。信頼できるクライアントを使用してください。
脅威となる主体、対策、制限の詳細については、docs/threat-model.md を参照してください。
これを作った理由
自分のインフラを運用していて、Claude を接続したいと思っていました。既存の選択肢はどれも、「役に立たない」か「root シェルをどうぞ。幸運を祈ります」のどちらかを選ばせるものでした。opsgate はその中間です。エージェントには実用的な機能を与えつつ、拒否権と実行記録は自分で保持できます。
貢献
貢献を心から歓迎します。アーキテクチャ上、新しいツールの追加も簡単です。ほとんどは約20行で実装できます。重要なルール(コマンドは argv を使い、文字列を連結しないこと)について、CONTRIBUTING.md をお読みください。
初めての貢献におすすめの項目
- 構造化されたツールの追加 — 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 サーバーはツール・リソース・プロンプトテンプレートを提供します。標準メッセージでやり取りするため、1 つのサーバーを複数のクライアントで再利用できます。