📝 この記事は forge.workstyle.tech に掲載した記事の転載です。
自分のPCで、複数社の無料枠を一つのモデル名で使うLLM中継を動かしていました。ポート4000が*:4000で待ち受け、同じLANなどから接続できる状態でした。起動スクリプトの待ち受け先が全ネットワークを指し、キーもブログに載せた仮の既定値のままでした。
先に結論
ローカルLLM中継は、ポート4000を全ネットワーク(*:4000)で待ち受け、ブログに載せた仮の既定キーで使える状態でした。プロンプトインジェクション(AIへの指示を外部の文章で乗っ取ろうとする攻撃)の点検中に気づき、待ち受け先をループバックアドレス(そのPC自身を指す接続先)に限定してキーを交換しました。再起動と新しいキーでの確認は自動許可の仕組みに止められたため、最後は私がコマンドを実行しました。
プロンプトインジェクションの点検で何が見つかったか
私はエージェントに、プロンプトインジェクションの点検を頼みました。プロンプトとはAIに渡す指示文のことです。点検では、その指示だけでなく周辺の設定や権限も確認していました。
そこで中継の待ち受け状態が見つかりました。起動方法を確認すると、ポートは全ネットワークで待ち受けていました。同じLANやTailscaleにいる人が、そのキーで中継を使えてしまう状態です。利用者を確かめるためのキーも仮の既定値のままで、私はその値をブログ記事に載せていました。
自分の文章に値を書いた時点で、それは秘密ではなくなっていました。値をブログから消しても、すでに設定された中継の状態は変わりません。待ち受け先とキーを直す必要がありました。
同じ点検では、ほかにも気になる状態が見つかりました。SSHの鍵に付けた制限が効いていませんでした。中継が落ちたときの最終手段として呼ぶ処理には、ファイルを読む権限が残っていました。検索結果をそのままLLMに渡していました。
どれも同じ点検で見つかったことですが、ここでは中継と最終手段の呼び出しに絞ります。調査中に問題を全部並べると、最初に直す場所がぼやけます。私はまず、誰が中継へ接続できる状態なのかを直すことにしました。
中継の待ち受け先とキーをどう直したか
起動スクリプトを修正し、待ち受け先をループバックアドレスに限定しました。これで、中継が全ネットワークで待ち受ける状態を改めました。
次に、キーを推測されにくいものへ替えました。キーは本人だけが読めるファイルに置き、スクリプトからそのファイルを読むようにしました。キーをコードに直接書いていたスクリプトも同じように直しました。
キーが見つからないときの処理も見直しました。以前は既定値を使う経路が残っていましたが、その経路はなくしました。キーを読めないときに、知られた既定値へ戻るのは避けたいと考えたからです。
変更前のファイルは、日付付きのバックアップに残しました。変更後、中継が新しいキーで使えるか確認しようとしましたが、最後の一手で止まりました。
再起動と待ち受け状態はどう確認したか
開発エージェントに、中継の再起動と、新しいキーで通るかの確認を頼みました。エージェントは手順を進めようとしましたが、秘密情報を扱う操作として、自動許可の仕組みに止められました。
私は一瞬、作業が中断したと感じました。修正そのものは済んでいて、残っているのは起動し直して確認することです。ところが、その確認には秘密を使う操作が含まれています。自動で続けられないのは、手順が曖昧だからではありませんでした。
エージェントは、人が実行するコマンドを二つ示しました。
launchctl kickstart -k gui/$(id -u)/<ラベル>
lsof -nP -iTCP:4000 -sTCP:LISTEN
一つ目で中継を再起動し、二つ目でポートの待ち受け状態を確認します。ラベルは実際の環境に合わせて入れる必要があります。私は人の操作でコマンドを実行しました。
2026-10-17時点で、lsofの結果はループバックアドレスの4000番ポートでLISTENでした。少なくとも、確認した時点では全ネットワーク待ち受けではありませんでした。キーを新しくし、キーが見つからないときに既定値へ戻る経路もなくしました。
中継が落ちたときの呼び出しから何を取り除いたか
中継が落ちたときには、別のLLM呼び出しを最終手段として使っていました。点検で、その処理にファイルを読む権限が残っていると分かりました。中継の障害対応に必要な処理へ、ファイルを読む権限まで渡す理由はありません。
私はその呼び出しにツールを渡さない設定にしました。MCP(LLMに外部ツールをつなぐ仕組み)と設定を読まず、空の一時フォルダで動くようにもしました。その状態で「ファイルを読んで」と頼んでも、中身は一切出てきませんでした。
これは、点検した範囲ではファイルを読めなかったという確認です。あらゆる状況で何も読めない、と言えるわけではありません。ただ、呼び出しの用途に合わせて持たせる権限を減らす手順を確認できました。
検索結果の扱いも変えました。検索結果を<search_results>で囲み、中に書かれた命令に従わないよう指示します。偽の閉じタグを取り除くようにもしました。検索結果をそのまま渡していた状態から、内容を区切り、指示として扱わない方針を明示しました。
秘密を使う最後の確認は誰が実行するか
この作業で、私は二つのことを確かめました。中継がどこで待ち受けるか、そして中継や最終手段の呼び出しにどんな権限が渡っているかです。
ローカルで動いていることは、ローカルの人しか使えないことを意味しません。待ち受け先を確かめなければ、私のように「自分用だから」と思い込んだまま使い続けることがあります。キーがブログに出ていたなら、その値を秘密として扱い続ける意味もありません。
一方、修正後の確認には人の操作が残りました。エージェントは再起動のコマンドを示せましたが、新しいキーを使う確認は自動許可の仕組みに止められました。私はそれを回避せず、人が実行する手順として受け取りました。
AIエージェントに秘密を扱わせる境界は、作業を最後まで自動で進められるかだけでは決まりません。どの権限を渡すか、どの操作を人が確認するかを、作業ごとに決める必要があります。今回は最後の一手を人が実行しました。その判断まで含めて、ローカルの中継を直す作業でした。
まとめ
- ポート4000の待ち受けを調べ、
*:4000になっていないか確認する - 起動スクリプトの待ち受け先をループバックアドレスに限定する
- 仮の既定キーを交換し、本人だけが読めるファイルから読む
- キーが見つからないとき、既定値へ戻る経路をなくす
- 中継を再起動し、
lsof -nP -iTCP:4000 -sTCP:LISTENで状態を確認する