ノート仕事とAI

「ローカルで動かす」と「外に出ない」は別のこと

仕事で使うローカルの言語モデルを、決めた1つだけに絞り、外への通信を可能な範囲で遮断して動かすためにWindows用のスクリプトを書いた記録。境界をFirewallに置いた理由、ルールの「形」まで検査するようにした経緯、実機で通したときに最初に止まった場所と、遮断を確認できた範囲。

仕事とAI 開発記録

Read this note in English

手描きの図。ノートパソコンの画面に「LM Studio」と書かれ、そこから右へ伸びる3本の線はどれも X で行き止まりになっている(上が LAN、下が Internet)。本体の下からは青い線が出て、localhost 127.0.0.1 と書かれたループを描いて同じ本体へ戻っている

仕事でローカルの言語モデルを使うために、Windows 用のスクリプトを3本書きました。やりたかったのは、決めた1つのモデルだけを使い、LM Studio 関連のプロセスが PC の外と通信するのを、可能な範囲で遮断することです。PC 全体の通信を止めるものではありません。

作ってみて一番はっきりしたのは、当たり前の分離でした。ローカルで動かしていることと、外に出ていないことは別です。アプリは更新確認やモデルの検索、プラグインの取得で通信します。モデルが手元にあることは、その通信が起きない理由になりません。

作ったもの

3本構成にしました。

スクリプト役割
セットアップFirewall ルールを作り(既定の構成の場合)、設定 JSON を安全側の値へ更新し、状態を記録する
起動記録した状態とルールを監査してから起動し、承認したモデルだけを読み込む
復元検証済みのバックアップを照合して設定を元に戻す

どれもダウンロードはしません。アプリもランタイムもモデルも、事前に配置済みである前提です。モデルは共有フォルダに置いたものをセットアップがシンボリックリンクとして登録するので、使う人がモデル名を調べたり、アプリ側の設定を触ったりする必要はありません。

境界をどこに置くか

設定 JSON には、ネットワークに関わる項目があります。開発者モード、ローカルサービスの有効化、プロキシ経由の検索、ダウンロード用のトークン。これらを安全側の値へ更新することには意味がありますが、境界にはなりません。アプリの更新で戻ることがありますし、項目の名前も意味も内部の仕様で、こちらから固定できないためです。

そこで Windows Firewall を境界にして、JSON は二重目の防御に置きました。 Firewall はアプリの外側にあり、アプリが自分で書き換えることはできません。JSON のほうは、Firewall が効いている前提での「念のため」に降格しました。

Firewall なら万全という話ではありません。規則は実行ファイルのパスに結びつくので、アプリやランタイムを更新して新しい実行ファイルができれば、その分は規則の外に出ます。既存の規則が消えるのではなく、守る対象から外れる、という形の穴です。

もうひとつ、この境界を自分で持つかどうかは配布時に選べるようにしました。既定は持つ側で、ここから先はその構成の話です。会社の Firewall や EDR に任せる設定を選んだときは、規則を作らず、監査もせず、こちらが確かめたとは表示しません。

この整理をしたあと、設定ファイルの扱いも変わりました。リポジトリには方針を書いた baseline ファイルを置いていますが、このファイルを書き換えても実効は変わりません。実際に適用する値はスクリプトの中にあり、スクリプト自身が検証します。設定ファイルを正にしてしまうと、そのファイルを差し替えるだけで防御を弱められるためです。baseline は、人が読んで方針と突き合わせるための対訳表として置いています。

ルールは、あるかどうかより形

Firewall のルールを作ること自体は難しくありませんでした。難しかったのは、そのルールがまだ効いているかを確かめるほうです。

ルールが1件存在していても、こういう状態がありえます。

  • プロトコルが TCP だけになっている
  • 適用プロファイルが Private だけに限定されている
  • リモートポートが 443 だけに絞られている
  • 無効になっている

どれも「ルールはある」と表示されます。けれど穴は開いています。

そこで起動側の監査を、存在ではなく形を見るように変えました。プロファイル、プロトコル、ローカルとリモートのポート、インターフェース種別、サービス、対象の実行ファイル、アドレス範囲を照合して、ひとつでも欠けていたら起動を止めます。テストでは上の3つを順に仕込んで、それぞれが拒否されることを確かめました。

ループバックだけ通す書き方

「ループバック以外をブロック」という指定は、ルールとして直接は書けませんでした。除外ではなく、範囲を割って書くことになります。

0.0.0.0-126.255.255.255
128.0.0.0-255.255.255.255
::2-ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff

127.0.0.0/8 の手前で切り、その先から再開する。IPv6 は ::1 の次から始める。結果として通るのは IPv4 の 127.0.0.0/8 と IPv6 の ::1 で、この3行が「ローカルだけ通す」の実体になっています。

自動化しなかったところ

セットアップは、LM Studio が動いていると止まります。ここでプロセスを終了させれば自動で進められますが、やめました。保存していない作業が消えるかもしれませんし、書き込みの途中かもしれないからです。

復元側も同じ考え方にしました。受け付けるのはセットアップ時に作った最初のバックアップだけで、起動時に作られたものは選びません。新しいバックアップがあっても、古い元バックアップのほうを選びます。ハッシュが合わなければ止まります。別のプロファイル宛なら止まります。管理下のディレクトリの外を指定されても止まります。

Firewall のルールのほうは、復元しても既定では残すことにしました。設定を戻すことと、通信を開けることは別の判断だからです。ルールまで消すのは、明示的に指定したときだけにしています。会社側へ委任する設定に切り替えたときは、セットアップがこのプロジェクトの作った規則だけを消します。Windows Firewall 本体や、会社が入れた規則には触れません。

結果として「止まる」で終わることが多い作りになりました。この種の道具では、条件を満たさないときに何もしないほうが正しい応答だと考えています。

名前ではなく、場所で見つける

動いているランタイムを探すとき、最初はプロセス名で探すのが自然に思えます。ただランタイムは種類が増えますし、名前も変わります。

テストで知らない名前のプロセスを置いてみて、名前ではなくパス——プロファイル配下の実行ファイルかどうか——で判定するようにしました。名前を知らなくても、その場所で動いているものは対象になります。

実機で最初に止まったところ

ここまでは自動テストの話です。テストは2種類書きました。静的な検査と、模擬した振る舞いの検査。どちらも通っていました。ただし UAC の承認も、本物の Firewall も、GUI の起動も、自動テストは触っていません。

実際の Windows 機で通してみると、最初に止まったのは、そのテストが見ていない場所でした。セットアップが、LM Studio のサービスを裏側で起こす段階から先へ進みません。起動中の表示のまま返ってこない。古いロックファイルを消しても同じでした。設計どおり止まって終わりましたが、安全に止まることと、使えることは別です。

そこで、この依存そのものを外しました。モデルとランタイムの検証を、セットアップから初回の安全起動へ移しています。セットアップは「モデルは未確定」という状態まで記録して終わり、最初の起動が通常ユーザーで GUI を立ち上げ、ランタイムとメモリの見積りを確かめ、読み込めたところで初めて確定させます。

外部通信は止まったのか

そのうえで通しました。今回確認した1台・1構成では、対象にした外部通信は遮断されていました。

確かめ方は、Firewall の画面を眺めることではなく、実際に通信させることでした。更新の確認、モデルの検索とカタログの取得、コマンドライン側からの取得——いずれも fetch failed で失敗します。ログにも同じ文字列が残りました。

TCP の待受けとして残っていたのは、2つだけです。

127.0.0.1:41343   LM Studio の内部 API
127.0.0.1:63377   推論ランタイム

**以前あった 0.0.0.0:8080 は、消えていました。** 冒頭に書いた「ローカルで動かしていることと、外に出ていないことは別」は、この差のことです。全インターフェース宛に待ち受けていたものが、スクリプトを通したあとには無くなっている。公開 API のほうも、自動起動を切り、待受け先を 127.0.0.1 に固定してあります。

起動側は、GUI の準備ができた時点とモデルを読み込んだ時点の2回、実際の TCP 待受けを検査します。localhost 以外で待ち受けていれば、起動を失敗扱いにします。ルールの形を照合するだけでなく、結果のほうも見るようにしました。

それでも preview のまま

確かめられたのは、1台の Windows 機で、1つの構成で、1回通したという範囲です。Windows と LM Studio のバージョン、GPU、ランタイムの組み合わせを網羅したわけではありません。バージョンは preview のままにしてあります。

残っている制約

  • Firewall ルールは実行ファイルのパスに結びつきます。アプリやランタイムを更新すると新しい実行ファイルができ、ルールの外に出ます。起動側はそれを検出して止まりますが、直しはしません。更新のたびにセットアップをやり直す必要があります
  • ループバックは通しています。認証のないローカルサーバを開けば、同じPC上の別のプロセスからは届きます
  • モデルを共有フォルダから読む構成では、LAN と SMB の通信が必要です。規則の対象は LM Studio 関連の実行ファイルなので、Windows 自体が行うファイル共有の通信までは止めません。完全に切り離すなら、モデルをローカルに置くことになります
  • 会社の管理端末では、グループポリシーでローカルのルール追加が禁じられていることがあります。その場合は、保護できたと言わずに止まります。会社側の仕組みに任せる設定も用意しましたが、そちらを選んだときは、このスクリプトが確かめたとは表示しません
  • ローカルの管理者権限を持つソフトや、カーネルで動くものは、この仕組みの外側にあります

サンドボックスではありませんし、VM でもコンテナでもありません。すでにあるアプリを、Windows が元から持っている仕組みで少し囲っただけのものです。

それでも、ローカルで動かしていることと外に出ていないことの間にあった距離は、いくらか埋まったと思っています。

スクリプトは MIT で公開しています — lmstudio-windows-hardening

タグ: ローカルLLM・Windows・生成AI・設計判断

← ノート一覧へ