mcpField
O
opsgate

opsgate

ローカル
polymatx.devクラウド・インフラ12 週間前に更新

opsgate について

opsgateは、汎用シェルを渡さずにAIエージェントがインフラを調査・管理できるMCPサーバーです。型付きツール、ポリシーモード、変更時の承認、改ざんを検知できる監査ログでホスト操作を制御します。

主な機能

  1. 01シェル文字列を使わない型付き運用ツール
  2. 02observe、operate、fullのポリシーモード
  3. 03変更操作の承認ゲート
  4. 04ホスト別・ツール別の対象制限
  5. 05ハッシュチェーン監査ログ
  6. 06ローカルstdioと認証付きHTTP

活用シーン

  1. 01サービス、ポート、プロセス、ディスク使用量の調査
  2. 02Dockerコンテナとシステムジャーナルの確認
  3. 03サービスやコンテナの変更を承認・監査

MCP 対応

stdioまたはHTTPで、型付きインフラツールをMCPクライアントに公開します。変更操作はMCP elicitationによる人間の承認を要求でき、observeモードでは読み取り専用です。

設定

ローカルサーバーの設定はどのクライアントでも共通です。コピーしてクライアントの MCP 設定ファイルに貼り付けてください。

mcp.json · 共通設定
{
"mcpServers": {
"polymatx-opsgate": {
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"ghcr.io/polymatx/opsgate:0.1.3"
]
}
}
}
起動コマンドdocker run -i --rm ghcr.io/polymatx/opsgate:0.1.3

ローカルサーバーにはマシン上の実行環境(Node.js 18 以上など)が必要です。設定ファイルの場所はクライアントごとに異なります。

パッケージ
ocighcr.io/polymatx/opsgate:0.1.3

ツール

ツール一覧はまだ取得されていません

ローカルサーバーのツールはマシン上で起動して初めて取得できます。メンテナーはまだツール一覧を提供していません。

詳細

opsgate

AI エージェントとサーバーの間に設ける安全ゲートです。

Claude、Cursor、またはあらゆる AI エージェントが、root シェルを渡されることなく 実際のインフラを操作できる MCP サーバーです。

CI Go Report Card Go Reference License: MIT Go 1.26+ MCP

動作イメージ · 30 秒で試す · 自分で試す · ツール · セキュリティモデル · コントリビューション

opsgate に対するエージェントの呼び出し:シェルインジェクション、パストラバーサル、observe モードでの再起動はすべて拒否され、何も実行されません。それぞれの試行はハッシュチェーン形式の監査ログに記録されます

動作イメージ

質問すると、エージェントは読み取り専用ツールで調査して回答します。

あなた: サーバーのポート 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 Codestdio同様に、ローカルプロセスとして動作します
クラウドまたはサンドボックス環境(ウェブチャット、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 を参照してください。

コメント

MCP は初めてですか?

プロトコルの概要から最初のサーバーの導入まで、よくある疑問にまとめて答えます。

AI アプリケーションが外部のツールやデータソースに統一的な方法で接続するためのオープンなプロトコルです。クライアントはモデルとの対話を担い、MCP サーバーはツール・リソース・プロンプトテンプレートを提供します。標準メッセージでやり取りするため、1 つのサーバーを複数のクライアントで再利用できます。