1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AlmaLinux + Podman + MCP で、しゃべってコンテナを操作する自作MCPサーバの構築

1
Last updated at Posted at 2026-06-06

手元のPCから自然言語でVPSのコンテナを操作する基盤を作った(Rootless Podman × MCP)

IT企業で働く、しがないサラリーマンです。

「手元のPCから自然言語で、VPS上のコンテナを安全に操作できたら気持ちよくない?」という思いつきを、週末にVPSで形にしてみました。
ポイントは、動くことより「攻撃されても被害を最小化すること」を最優先で設計したところです。

この記事では、構成・なぜPodmanなのか・塞ぐべき3つの穴・構築手順・ハマったところまで、一通りまとめます。


作ったもの

手元PCで自然言語を打つと、MCP(Model Context Protocol)経由でVPS上のRootless Podmanが動き、コンテナを操作してくれる——という基盤です。

手元PC(Mac / MCPクライアント + LLM)
   |  SSHトンネル(暗号化・localhostのみ)
   v
VPS(AlmaLinux 10)
   +- MCPサーバ(podman-mcp-server / localhostバインド)
   +- Rootless Podman(rootなしでコンテナ実行)

MCPサーバのポートは外部に一切公開せず、SSHトンネル経由でのみ到達できるようにしています。


なぜ Docker ではなく Podman なのか

最大の理由は「rootless を後付けではなく前提にできる」ことです。
この基盤で一番こわいリスク=権限昇格(後述の①縦)を、設計段階から抑え込めます。

観点 Docker Podman この基盤での意味
デーモン dockerd(常時 root で常駐) デーモンレス(実行時のみ) 常駐 root プロセスという攻撃面が無い
権限モデル docker グループ ≒ 実質 root rootless が標準 ①縦(権限昇格)を設計段階で抑制
ホスト脱出 デーモン経由のリスク ユーザ権限内で完結 破られても mcprun 権限で止まる
systemd常駐 追加設定が必要 親和性が高い(Quadlet等) 常駐化が素直に書ける
互換性 Docker CLI / OCI 互換 学習コスト低・既存イメージ流用可
RHEL系 標準採用(dnf で導入) AlmaLinux 10 でそのまま入る

dockerグループ問題
Docker の docker グループ付与は「実質 root 権限を渡す」有名な権限昇格経路です。
Podman はそれ自体が不要で、一般ユーザのまま完結します。


設計の軸:塞ぐべき3つの穴

設計のよりどころにした3つのリスクです。
各STEPはこのいずれかに対応しています。

リスク 内容 対策
①縦(権限昇格) コンテナからホストへ脱出される Rootless Podman
②逆流(注入) 悪意ある指示・イメージでツールを勝手に実行される 人間承認 + digest固定
③横(踏み台) 侵入後、社内網やクラウドメタデータへ横移動される egress遮断

構築手順

STEP 1. 初期設定

専用ユーザを作り、SSHは鍵認証のみ、ファイアウォールはSSHだけ通す最小構成にします。

sudo useradd -m mcprun                 # コンテナ実行用の専用ユーザ
# /etc/ssh/sshd_config: PasswordAuthentication no (鍵認証のみ)
sudo firewall-cmd --permanent --add-service=ssh
sudo firewall-cmd --reload             # 開けるのは SSH だけ

STEP 2. Rootless Podman 導入

sudo dnf install -y podman
sudo loginctl enable-linger mcprun     # ログアウト後もサービス維持
systemctl --user enable --now podman.socket   # Podman REST API ソケット
podman --version                       # 例: 5.8.2

Rootless とは
一般ユーザ権限でコンテナを動かす方式です。
万一コンテナを破られても、奪われるのは mcprun 権限までで、root は取られません。(①縦の対策)

STEP 3. MCPサーバを常駐化する

manusa/podman-mcp-server(Go・Apache-2.0)を localhost のみで待受させ、systemd user サービスで常駐させます。

~/.config/systemd/user/podman-mcp.service

[Unit]
Description=Podman MCP Server (localhost only)
After=podman.socket
Wants=podman.socket

[Service]
ExecStart=/bin/bash -c 'exec /usr/local/bin/podman-mcp-server \
  --port 8000 --podman-impl api < <(tail -f /dev/null)'
Restart=always
RestartSec=3

[Install]
WantedBy=default.target
systemctl --user daemon-reload
systemctl --user enable --now podman-mcp.service
curl -s -m 2 -o /dev/null -w 'http=%{http_code}\n' http://127.0.0.1:8000/sse

ハマりどころ①:起動直後に即終了(status=0)
systemd は stdin に /dev/null を渡すため、MCPサーバが stdin の EOF を検知して「正常終了」してしまいます(HTTPモードでも発生)
解決: < <(tail -f /dev/null) で EOF の来ない stdin を食わせ続けます。
   上の ExecStart がその対策版です。

STEP 4. 横移動(踏み台化)を遮断する

侵入されても外へ出ていけないよう、出口(egress)を絞ります。
firewalld の direct ルールを使います。

UID_MCP=$(id -u mcprun)
# クラウドメタデータ(169.254.169.254)を遮断
sudo firewall-cmd --permanent --direct --add-rule ipv4 filter \
  OUTPUT 0 -d 169.254.169.254 -j DROP
# 例外:ループバック・コンテナサブネット(優先度0=先に評価)
sudo firewall-cmd --permanent --direct --add-rule ipv4 filter \
  OUTPUT 0 -o lo -j ACCEPT
sudo firewall-cmd --permanent --direct --add-rule ipv4 filter \
  OUTPUT 0 -d 10.88.0.0/16 -j ACCEPT
# mcprun の私的アドレス宛て(社内網)を遮断(優先度1)
for NET in 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16; do
  sudo firewall-cmd --permanent --direct --add-rule ipv4 filter \
    OUTPUT 1 -m owner --uid-owner $UID_MCP -d $NET -j DROP
done
sudo firewall-cmd --reload

必ず自分の環境を確認してから適用
デフォルトGWやDNSが私的アドレス帯だと、このルールで通信が壊れます。
確認コマンド:ip route(GW)/ /etc/resolv.conf(DNS)/ podman network inspect podman(サブネット)
私の環境ではGW・DNSが公開IP、コンテナのみ 10.88.0.0/16 だったため、そこだけ例外許可しました。

検証:MCPは http=200 のまま、メタデータ宛ては rc=28(タイムアウト=遮断成功)

STEP 5. SSHトンネル + クライアント接続

MCPポートは外部非公開のまま、SSHの暗号路だけで localhost:8000 に到達させます。

# 手元PC:このタブは繋ぎっぱなし(閉じるとトンネルも切れる)
ssh -N -L 8000:127.0.0.1:8000 -i ~/.ssh/your-key.pem mcprun@<VPS_IP>

MCPクライアント設定(claude_desktop_config.json

{
  "mcpServers": {
    "podman-vps": {
      "command": "npx",
      "args": ["-y", "mcp-remote",
               "http://127.0.0.1:8000/sse",
               "--transport", "sse-only"]
    }
  }
}

ハマりどころ②:sessionid must be provided(400)
mcp-remote は既定で Streamable HTTP を先に試すため、SSE専用入口 /sse に POST して弾かれます。
解決: --transport sse-only を明示して SSE の作法で接続させます。


デモ:自然言語でコンテナを立てる

ここまでできたら、あとは手元PCで日本語を打つだけです。
試しに nginxdemos/hello を立てて、一覧を出してもらいました。

起動してるコンテナ一覧見せて

スクリーンショット 2026-06-06 15.40.14.png

nginxdemos/hello(カラフルなデモページを返すWebサーバ)が running 状態、ポートは 8080 → 80 で公開——と、自然言語の問いかけにツール連携で答えてくれているのが分かります。

digest固定で Rug Pull を防ぐ
デモでは :latest を使っていますが、本番では nginxdemos/hello@sha256:… のようにダイジェスト固定するのがおすすめです。
タグは中身をすり替えられる(Rug Pull)ため、@sha256:… で「この中身」を釘付けにします(②逆流の対策)。

ブラウザでこのページを見たいときは、MCP用(8000)とは別にもう1本トンネルを張ります。

ssh -N -L 8080:127.0.0.1:8080 -i ~/.ssh/your-key.pem mcprun@<VPS_IP>

その状態で http://localhost:8080 を開けばOK!
nginxdemos/hello のページが表示されます。

スクリーンショット 2026-06-06 15.51.40.png

ファイアウォールはSSHしか開けていないので、外部から直接 VPS_IP:8080 には繋がりません。
SSHの暗号路の中だけで届く二重の守りになっています。


ハマりどころ一覧

症状 原因 解決
サービスが起動直後に即終了(status=0) stdin が /dev/null で EOF を検知し正常終了 < <(tail -f /dev/null) で stdin を保持
接続失敗 sessionid must be provided(400) mcp-remote が /sse に Streamable HTTP で POST --transport sse-only を明示
curl /sse が返ってこない /sse はストリーミングで開きっぱなしが正常 -m 2 でタイムアウト健診に切替

運用ルール

  1. 使うときだけトンネルを開く — 終わったらSSHタブを閉じる。閉じれば外部経路はゼロ
  2. 読み取り系は常時許可 / 変更系は毎回目視listlogsinspect は許可しっぱなしでも被害ゼロ。runremovepull は毎回ダイアログを確認
  3. イメージは digest 固定:latest ではなく @sha256:… を使い、Rug Pull(中身すり替え)を防ぐ
  4. 承認ダイアログは引数まで読む — ②逆流(ツール注入)の最後の砦。表示文ではなくダイアログの引数が唯一の根拠

セキュリティ注記
本記事中の <VPS_IP> および ~/.ssh/your-key.pem はプレースホルダです。
実IP・実鍵名・ホスト名は記事に載せず、別途管理してください。


まとめ

  • ①縦 = Rootless Podman
  • ②逆流 = 人間承認 + digest固定
  • ③横 = egress遮断

この三重の対策で、自然言語によるコンテナ操作環境を組みました。
同じことをやってみたい人の参考になればうれしいです。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?