はじめに
個人で運営している「ねこ宿研究所」では、AIコーディングエージェントを使ってWordPressの記事、分類、プラグインを保守しています。
対象はDocker Compose上で動く本番WordPressです。便利さだけを優先してSSHとWP-CLIをそのまま渡すと、誤った引数ひとつで公開状態やデータを変えられます。一方、すべてを手作業に戻すと、定型的な確認や更新を自動化できません。
そこで、実行経路を次の4層に分けました。
AIエージェント
↓ 1. ローカルラッパー
SSH + 限定sudo
↓ 2. root所有のサーバーラッパー
一時的なWP-CLIコンテナ
↓ 3. 固定したネットワーク・ボリューム・実行ユーザー
WordPress
↓ 4. バックアップ・公開後確認
公開サイト
この記事では、実際に運用して分かった設計上のポイントを紹介します。ホスト名、ユーザー名、内部パス、認証情報は例示用に置き換えています。
1. AIからはローカルラッパーだけを呼ぶ
AIが実行する入口は、リポジトリ内のシェルスクリプトに固定しました。
#!/usr/bin/env bash
set -euo pipefail
PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
export PATH
if (( $# == 0 )); then
echo "Usage: ./wp.sh <wp-cli arguments>" >&2
exit 64
fi
args=()
for argument in "$@"; do
printf -v quoted '%q' "$argument"
args+=("$quoted")
done
ssh -o BatchMode=yes example-host \
"sudo -n /usr/local/sbin/example-wp ${args[*]}"
ここで意識したのは次の点です。
-
BatchMode=yesでパスワード入力待ちにしない -
sudo -nで対話的な昇格を許さない - 接続後に呼ぶコマンドを固定する
- 各引数を
printf %qでクォートする -
PATHを固定する
sudoersでは、この固定スクリプトだけを許可します。wp、docker、シェルそのものを許可しません。
operator ALL=(root) NOPASSWD: /usr/local/sbin/example-wp *
サーバーラッパーはroot所有にし、AIが接続するユーザーから書き換えられない状態にします。
2. 危険度が特に高いWP-CLIコマンドを入口で拒否する
サーバー側では、任意コード実行、直接SQL、設定値の表示につながるコマンドを拒否しています。
case "${1}:${2-}" in
eval:*|eval-file:*|shell:*|package:*|db:cli|db:query|config:get|config:list)
echo "This WP-CLI command is blocked." >&2
exit 65
;;
esac
ここは正直にいうと、厳密な許可リストではありません。投稿や分類、プラグインなど、運用に必要なWP-CLIのサブコマンドを広く使うため、特に危険な経路を拒否する方式です。
そのため、この層だけを「安全装置」とは考えていません。削除や更新を本当に強く制限したい環境では、post-listやcreate-draftのような独自操作名だけを受け付け、内部で固定コマンドへ変換する許可リスト方式のほうが堅牢です。
実運用では、記事公開専用スクリプトも別に用意し、引数の個数、原稿ファイルの存在、front matter、投稿状態を検査してからWP-CLIを呼びます。自由なコマンドと定型ワークフローを分けることで、日常操作のミスを減らせました。
3. WP-CLIは一時コンテナで実行する
ホストへWP-CLIを常駐させず、公式のWordPress CLIイメージを実行時だけ起動します。
docker run --rm \
--user 33:33 \
--network example_wordpress_net \
--volumes-from wordpress_app \
--mount "type=bind,src=/srv/wp-staging,dst=/wp-staging,readonly" \
--env-file "$ENV_FILE" \
wordpress:cli-php8.2 \
wp --path=/var/www/html "$@"
固定した項目は次のとおりです。
- コンテナイメージ
- WordPressコンテナと同じネットワーク
- WordPressのボリューム
- WordPress内の実行ユーザー(UID/GID 33)
- WordPressのパス
- ローカルから取り込むステージング領域(読み取り専用)
docker run自体をAIへ開放しないことが重要です。Dockerソケットへアクセスできるユーザーは、実質的にホスト上で非常に強い権限を持ちます。AI用ユーザーをdockerグループへ追加せず、root所有の固定ラッパーだけがDockerを呼ぶ構成にしました。
4. 認証情報は一時ファイルへ絞って渡す
WP-CLIコンテナにはデータベース接続情報が必要です。ただし、Composeの環境変数を全部コピーすると、WordPress以外の秘密情報まで渡しかねません。
そこで、既存コンテナの環境変数から必要なキーだけを抽出し、権限を絞った一時ファイルへ書き出します。
ENV_FILE=$(mktemp /var/tmp/example-wp-env.XXXXXX)
trap 'rm -f -- "$ENV_FILE"' EXIT HUP INT TERM
umask 077
docker inspect --format '{{range .Config.Env}}{{println .}}{{end}}' \
wordpress_app |
grep -E '^(WORDPRESS_DB_HOST|WORDPRESS_DB_USER|WORDPRESS_DB_PASSWORD|WORDPRESS_DB_NAME|WORDPRESS_TABLE_PREFIX)=' \
> "$ENV_FILE"
ポイントは、値を標準出力やログへ表示しないこと、一時ファイルを必ず削除すること、最初からumask 077を設定することです。
なお、より強い分離が必要なら、Docker Secretsや専用の資格情報管理を検討すべきです。この実装は、既存のCompose環境を大きく変えずに露出範囲を狭めるための現実的な折衷案です。
5. 技術的な制限と運用ルールを重ねる
ラッパーで止められない誤りもあります。たとえば、正しいWP-CLIコマンドで間違った記事を更新するケースです。
ねこ宿研究所では、リポジトリの運用ルールとして次を固定しています。
- 調査は読み取り専用から始める
- WordPress変更前に手動バックアップを取得する
- 原稿はローカルのMarkdownを正本にする
- 公開後に投稿データを再取得する
- HTTP応答とPC・スマートフォン表示を確認する
- 失敗時は追加変更を止める
文章によるルールは技術的な権限制御の代わりにはなりません。しかし、技術的に許可された操作の中で「何を、いつ、どの順番で行うか」を制御する役割があります。
この実装を含む実際の運用背景は、ねこ宿研究所のAI・WP-CLI運用事例にも整理しています。
実際に運用して分かったこと
拒否できることもテスト対象にする
正常系だけでなく、eval、db query、config get、引数なし実行が失敗することを確認します。「実行できた」だけでなく「実行してはいけない操作が失敗した」をテスト結果に含める必要があります。
万能ラッパーを作らない
バックアップ、WordPress操作、Docker監査、メディアのステージングを別コマンドに分けました。ひとつのラッパーへ機能を集めると、sudoで許可した入口の権限が徐々に広がります。
バックアップ成功と復元可能性は別
アーカイブが作成されたことだけでは不十分です。gzip、tar、チェックサム、DBダンプの完了マーカー、必要ファイルの収録を検査しています。さらに、復元手順は別途確認する必要があります。
AIに見せる出力も最小化する
設定ファイルや環境変数を取得できるコマンドを止めても、通常のコマンド出力やエラーログへ秘密情報が混じる可能性は残ります。コマンドの権限だけでなく、何をチャットやログへ返すかも設計対象でした。
まとめ
本番WordPressをAIで運用する際、私たちは次の4層を重ねました。
- ローカルの固定ラッパーと限定sudo
- root所有のサーバーラッパーによる高危険度コマンドの拒否
- 一時WP-CLIコンテナのネットワーク、ボリューム、ユーザー固定
- バックアップ、正本管理、公開後確認という運用手順
現在のラッパーは運用範囲との兼ね合いで拒否リスト型です。より狭い用途なら、独自操作名から固定WP-CLIコマンドへ変換する許可リスト型を選びます。
重要なのは「AIへ注意するよう伝える」だけで終わらせず、誤操作が起きても影響範囲を限定できる実行経路を作ることでした。