This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
# 依存パッケージの外向き通信インベントリ
|
||||
|
||||
調査日: 2026-09-08
|
||||
基準コミット: `cf2bc52d6725da17b207202bdc6eaf7ac9f0cab7`
|
||||
|
||||
この文書は、root と UI の lockfile を全件機械集計し、展開済み依存のランタイム JavaScript/JSON をテレメトリ関連語彙で絞り込んだ初段の棚卸しである。通常利用時に依存が自動収集・送信する telemetry、analytics、crash report、usage report を主対象にする。利用者が明示した API 接続、WebFetch、通知、MCP、OAuth、LLM 接続などは対象外である。悪意ある文書や入力で通信を起こす攻撃経路も、この文書では扱わない。
|
||||
|
||||
## 結論
|
||||
|
||||
確認した依存候補から、通常利用時に自動送信される telemetry、analytics、crash report は見つからなかった。`@opentelemetry/api` と `@univerjs/telemetry` は lockfile に存在するが、前者には exporter、後者には送信実装がなく、アプリにも送信 provider の登録がない。
|
||||
|
||||
以前、通常 UI 操作で発生していた Google Fonts、jsDelivr の Monaco、unpkg の Leaflet 資産取得は、現在の変更でローカル npm 資産へ切り替えた。地図を開いた際の OpenStreetMap タイル取得は残る。これは telemetry ではないが、通常 UI 操作で第三者へ出る通信なので記録する。
|
||||
|
||||
この結論は下記の「確認済み」の経路に限る。語彙走査にヒットした全ファイルを手作業で監査したという意味ではなく、動的な全通信先監視や全依存の制御フロー監査も実施していない。
|
||||
|
||||
## Lockfile 集計
|
||||
|
||||
対象は lockfile v3 の `packages` である。空キーの root record、`link: true` のローカル workspace link、その link の実体を表す `node_modules/` 外の workspace record を各 lockfile の package 件数から除外した。`dev !== true` を production install に含まれる record、`dev === true` を development-only record とした。同一パッケージの複数版やネスト配置は別 record として数える。
|
||||
|
||||
| 対象 | 直接 production 宣言 | 直接 development 宣言 | package records | production に含む | development-only | 推移依存 records | 推移 production | 推移 development-only | 一意な名前 | 一意な名前+版 |
|
||||
|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|
|
||||
| root [`package-lock.json`](../../package-lock.json) | 44 | 22 | 633 | 446 | 187 | 568 | 400 | 168 | 551 | 598 |
|
||||
| UI [`ui/package-lock.json`](../../ui/package-lock.json) | 44 | 12 | 497 | 436 | 61 | 442 | 389 | 53 | 407 | 444 |
|
||||
|
||||
2 lockfile の合計は 1,130 package records で、同じ名前+版を両方の lockfile 間でも重複除外すると 984 組である。ローカル workspace link の `@aao/answer-worker-contract` とその実体 record は表から除外した。
|
||||
|
||||
集計時の SHA-256 は次の通り。
|
||||
|
||||
- `package-lock.json`: `c3a1e69f7160b1bcc29969d249ab7b9f34b7ea55b725409a7a3efb6dc0d8dad5`
|
||||
- `ui/package-lock.json`: `4c489f8f91a933a3cff6223b91a76dee570d29504c704139b47dbd4c57a49f2d`
|
||||
|
||||
直接依存の数は manifest の宣言数であり、package record の分類とは別軸である。例えば development に直接宣言されたパッケージが production 推移依存としても必要な場合、その record の `dev` は `true` にならない。
|
||||
|
||||
## 通常 runtime の外向き経路
|
||||
|
||||
| 状態 | 依存・経路 | 判定と根拠 |
|
||||
|---|---|---|
|
||||
| 確認済み・外向き通信あり | OpenStreetMap tiles | 地図詳細を表示し、場所が 1 件以上ある場合に `https://tile.openstreetmap.org/{z}/{x}/{y}.png` を取得する。タイル座標と通常のブラウザ要求メタデータが送られる。[`MapPlacesDetail.tsx`](../../ui/src/components/embed/MapPlacesDetail.tsx#L36-L55) |
|
||||
| 確認済み・ローカル化 | Leaflet 1.9.4 | JS、CSS、marker 画像を npm 依存から動的 import する。以前の unpkg script/CSS は使わない。[`MapPlacesDetail.tsx`](../../ui/src/components/embed/MapPlacesDetail.tsx#L1-L27) |
|
||||
| 確認済み・ローカル化 | Monaco Editor 0.55.1 | Monaco 本体と各 worker を npm 依存から import し、`loader.config({ monaco })` へ渡す。`@monaco-editor/loader` の既定 jsDelivr 経路は使わない。[`monaco-loader.ts`](../../ui/src/lib/monaco-loader.ts#L1-L32) |
|
||||
| 確認済み・ローカル化 | IBM Plex / JetBrains Mono | Fontsource の CSS とフォントファイルをビルド資産に含める。以前の Google Fonts CSS は使わない。[`index.css`](../../ui/src/index.css#L1-L10) |
|
||||
| 確認済み・送信なし | `prom-client` / `@opentelemetry/api` | `@opentelemetry/api` 1.9.1 は `prom-client` 15.1.3 の推移依存で、lockfile に SDK/exporter/instrumentation package はない。アプリは process metrics をローカル registry に収集し、受信した `/metrics` scrape に返す pull 型である。[`registry.ts`](../../src/metrics/registry.ts#L26-L48) [`metrics-subsystem.ts`](../../src/bridge/metrics-subsystem.ts#L31-L43) |
|
||||
| 確認済み・送信なし | `@univerjs/telemetry` 0.25.1 | 展開済み package は `ITelemetryService` identifier のみを公開する。`@univerjs/sheets-ui` は `sheetId`、`unitId`、描画時間統計を optional service へ `capture` できるが、アプリの登録プラグインに telemetry provider はない。[`UniverGrid.tsx`](../../ui/src/components/files/spreadsheet/UniverGrid.tsx#L263-L288) |
|
||||
| 確認済み・ローカル処理 | Mermaid / PDF | Mermaid は npm bundle を直接 import し、server renderer も `vendor/` または `ui/node_modules` の bundle を読む。[`FilePreviewContent.tsx`](../../ui/src/components/files/FilePreviewContent.tsx#L25-L28) [`mermaid.ts`](../../src/engine/tools/mermaid.ts#L123-L142) PDF はローカルファイルの Buffer を `PDFParse` に渡す。[`office.ts`](../../src/engine/tools/office.ts#L1673-L1679) |
|
||||
|
||||
## アプリ機能による通常の外向き通信
|
||||
|
||||
以下は自動 telemetry ではなく、設定済み機能を動かすための通信である。依存 package の存在だけでは発生せず、表の条件を満たしたときに有効になる。
|
||||
|
||||
| 機能 | 宛先と有効化条件 | 送信する主なデータ | 状態 |
|
||||
|---|---|---|---|
|
||||
| LLM 呼び出し | 設定した OpenAI 互換 endpoint。タスクがモデルを呼ぶと `POST <base>/chat/completions`。[`openai-compat.ts`](../../src/llm/openai-compat.ts#L1273-L1283) | API key、会話 messages、model、tools、生成設定 | 確認済み |
|
||||
| 設定画面の model 一覧取得 | model 設定画面で endpoint が入力されると、ブラウザから直接 `GET <endpoint>/models`。literal API key は Bearer header に入る。`env_ref` / masked key は取得を行わず、key を cleared にした場合は無認証で取得する。[`ModelSelect.tsx`](../../ui/src/components/settings/ModelSelect.tsx#L69-L96) | 設定中の endpoint、Accept header、literal の場合は API key | 確認済み |
|
||||
| LLM health / capacity probe | 設定した direct worker の `/slots`、`/health`、`/metrics`、proxy worker の `/health`。稼働中は約 5 秒ごと。direct probe は worker API key を送らず、proxy `/health` は設定された key を送る。[`backend-probes.ts`](../../src/engine/backend-probes.ts#L420-L480) [`backend-probes.ts`](../../src/engine/backend-probes.ts#L932-L944) | GET、Accept、proxy の場合は Authorization。応答から稼働状態・slot・throughput を読む | 確認済み |
|
||||
| OAuth と avatar | 管理者が Google または Gitea provider を有効にし、利用者がログインした場合。Gitea は設定 base URL の authorize/token/user/orgs、Google は Passport strategy の provider endpoint を使う。[`auth.ts`](../../src/bridge/auth.ts#L577-L672) provider が返した avatar URL は画面の `<img src>` からブラウザが取得する。[`UserAccountMenu.tsx`](../../ui/src/components/layout/UserAccountMenu.tsx#L67-L68) | OAuth client情報、authorization code/token、user profile/org照会。avatar取得時はURLと通常のbrowser metadata | 確認済み |
|
||||
| Web Push | 利用者が通知を有効にし push subscription がある場合、その subscription endpoint。[`push-service.ts`](../../src/push-service.ts#L176-L212) | 既定はevent種別、task ID、汎用文言。`includeDetails` opt-in時はtask titleとpiece/summaryも含む。VAPID署名とsubscription keysを使用 | 確認済み |
|
||||
| Space webhook | workspaceでDiscord/Slack/Teams webhookを登録・有効化し、対象eventが発生した場合、登録したHTTPS URL。[`webhook-service.ts`](../../src/webhook-service.ts#L419-L440) [`webhook-service.ts`](../../src/webhook-service.ts#L483-L527) | event、task ID、task URL。`includeDetails` 設定によりtask title、piece、space名。JSON POST | 確認済み |
|
||||
| MCP | 暗号化キーを設定し、HTTP MCP serverを登録して接続・tool list・tool callを行う場合、登録した server URL。Bearer または指定headerへtokenを付ける。[`client-factory.ts`](../../src/mcp/client-factory.ts#L34-L65) [`tool-executor.ts`](../../src/mcp/tool-executor.ts#L47-L62) | MCP初期化/capability、tool名と引数、認証token | 確認済み |
|
||||
| 起動時の不足資産補完 | `scripts/server.sh start` は毎回 `prepare.sh` を実行する。Chromium が無い場合は Playwright CDN から取得し、共有library不足時は `playwright install-deps` がOS package repositoryを使い得る。[`server.sh`](../../scripts/server.sh#L120-L127) [`prepare.sh`](../../scripts/prepare.sh#L27-L64) | Playwright/npm/OS標準のdownload request metadata。アプリのtask内容は送らない | 確認済み・条件付き |
|
||||
|
||||
`npm ci`、Docker build、`setup.sh`、`upgrade.sh` に伴う registry、Git、OS package、Playwright browser の取得は管理者が明示的に行う install/build/update 通信であり、通常 runtime の telemetry 判定には含めない。
|
||||
|
||||
## ローカル UI 資産の回帰検査
|
||||
|
||||
次の検査をリポジトリ root で再現できる。
|
||||
|
||||
```bash
|
||||
npm run security:ui-assets
|
||||
```
|
||||
|
||||
[`check-local-ui-assets.mjs`](../../scripts/security/check-local-ui-assets.mjs) は実際の `MonacoFileEditor`、`MapPlacesDetail`、`index.css` を一時 Vite アプリへ読み込み、production build を Chromium で実行する。2026-09-08 の検査は exit 0 で成功し、3 font family のローカル読込、Monaco の編集・保存と same-origin worker、Leaflet の 2 marker の表示・画像 decode を確認した。OpenStreetMap の tile request は検査内でローカル応答し、それ以外の cross-origin request と不要な WebSocket 接続は実接続前に遮断して失敗にする。画面表示後に Google Fonts を意図的に追加する negative fixture で、遅れて発生する要求も検出した。
|
||||
|
||||
フォントと地図タイルの要求件数は描画条件で変わるため、各実行の集計をコマンド末尾へ出力する。
|
||||
|
||||
この動的検査の範囲は上記 UI component と資産に限る。アプリ全画面の操作や server workload を動かして全通信を監視する検査ではない。
|
||||
|
||||
## テレメトリ語彙の候補確認
|
||||
|
||||
展開済み依存の `.js`、`.cjs`、`.mjs`、`.json` を走査し、source map は除外した。使用した語彙は `telemetr`、`analytics`、`sentry`、`datadog`、`newrelic`、`mixpanel`、`amplitude`、`posthog`、`rollbar`、`bugsnag`、`phone.?home`、`usage.?report`、`update.?check`、`license.?check`、`opentelemetry` である。
|
||||
|
||||
| 対象 | 展開済み lock records | lock と同一版 | 不一致 | 未展開 | 走査ファイル | 走査量 |
|
||||
|---|---:|---:|---:|---:|---:|---:|
|
||||
| root | 550 / 633 | 550 | 0 | 83 | 9,879 | 130,972,307 bytes |
|
||||
| UI | 469 / 497 | 469 | 0 | 28 | 15,076 | 258,538,747 bytes |
|
||||
|
||||
root と UI はこの worktree で `npm ci --ignore-scripts --no-audit --no-fund` 後の展開物を使った。存在した root 550 records と UI 469 records はすべて lockfile と同一版だった。未展開の root 83 records と UI 28 records はすべて `optional: true` で、現在の OS/CPU 以外の native package などを含む。
|
||||
|
||||
手動で送信経路まで確認した候補は次の通り。
|
||||
|
||||
- `@opentelemetry/api` とその導入元 `prom-client`: API とローカル registry だけで、exporter package と外向き送信 sink は確認されなかった。
|
||||
- `@univerjs/telemetry` と `@univerjs/sheets-ui`: identifier と optional `capture` 呼び出しはあるが、送信 provider の実装・登録は確認されなかった。
|
||||
- `monaco-editor`: telemetry という名前の内部型・service は含むが、MAESTRO が collector や転送先を登録する箇所は確認されなかった。現在は editor code と worker もローカル配信する。
|
||||
|
||||
`pdfjs-dist`、`pdf-parse`、Playwright、Mermaid、TypeScript、highlight.js、React、各種 CSS/parser package などにも語彙ヒットがあった。エラーサービス名を入力データとして扱う parser、CSS の `scrollbar` に含まれる文字列、テスト・開発コード、一般的な型名などが混在するため、これらは **keyword candidate のまま未検証** である。語彙ヒットだけを telemetry 実装の証拠にはしていない。
|
||||
|
||||
## Install script
|
||||
|
||||
lockfile の `hasInstallScript: true` を全件抽出した。これは install 時のコード実行面であり、通常 runtime の自動 telemetry とは別である。
|
||||
|
||||
| lock | package | 区分 | 展開・確認状態 | install 時の挙動 |
|
||||
|---|---|---|---|---|
|
||||
| root | `[email protected]` | 直接 production | 確認済み | `prebuild-install || node-gyp rebuild --release`。prebuild が無ければ GitHub release asset の取得を試み、失敗時はローカル build。 |
|
||||
| root | `[email protected]` | 直接 production | 確認済み | `node install.js` から optional crypto binding を `node-gyp rebuild`。script 自身に HTTP client はない。node-gyp はローカル header が無い環境では header 取得を行い得る。 |
|
||||
| root | `[email protected]` | 推移 production・optional | 確認済み | buildcheck 後に `node-gyp rebuild`。script 自身に HTTP client はない。 |
|
||||
| root | `playwright/node_modules/[email protected]` | 推移 production・optional | Linux では未展開、未検証 | lockfile flag のみ確認。macOS 向け optional package。 |
|
||||
| root | `[email protected]` | 推移 development-only | 確認済み | `node install.js`。対応 platform の optional binary が無ければ npm を起動し、それも失敗すると `https://registry.npmjs.org/@esbuild/*` の tarball を直接取得する。 |
|
||||
| root | `[email protected]` | 推移 development-only・optional | Linux では未展開、未検証 | lockfile flag のみ確認。macOS 向け optional package。 |
|
||||
| UI | `[email protected]` | 推移 production | 確認済み | `node scripts/postinstall` は親 manifest の semver 記法を検査して warning を出すだけで、通信しない。 |
|
||||
| UI | `[email protected]` | 推移 production・optional | Linux では未展開、未検証 | lockfile flag のみ確認。macOS 向け optional package。 |
|
||||
|
||||
通常の [`build-all.sh`](../../scripts/build-all.sh#L26-L31) は plain `npm ci` を使うため、該当 platform では lifecycle script が動く。Docker build は builder と runtime の両方で `npm ci --ignore-scripts` を使い、その後に検査済みの native rebuild を行う。[`Dockerfile`](../../Dockerfile#L15) [`Dockerfile`](../../Dockerfile#L126-L134)
|
||||
|
||||
## 再現方法
|
||||
|
||||
件数は次の Node.js スクリプトで再現できる。
|
||||
|
||||
```bash
|
||||
node <<'NODE'
|
||||
const fs = require('node:fs');
|
||||
for (const file of ['package-lock.json', 'ui/package-lock.json']) {
|
||||
const lock = JSON.parse(fs.readFileSync(file, 'utf8'));
|
||||
const root = lock.packages[''];
|
||||
const direct = new Set([
|
||||
...Object.keys(root.dependencies ?? {}),
|
||||
...Object.keys(root.devDependencies ?? {}),
|
||||
...Object.keys(root.optionalDependencies ?? {}),
|
||||
]);
|
||||
const rows = Object.entries(lock.packages)
|
||||
.filter(([path, value]) => path.includes('node_modules/') && value.link !== true);
|
||||
const isDirect = ([path]) => {
|
||||
const name = path.replace(/^node_modules\//, '');
|
||||
return direct.has(name) && path === `node_modules/${name}`;
|
||||
};
|
||||
const count = (predicate) => rows.filter(predicate).length;
|
||||
console.log(file, {
|
||||
directProduction: Object.keys(root.dependencies ?? {}).length,
|
||||
directDevelopment: Object.keys(root.devDependencies ?? {}).length,
|
||||
records: rows.length,
|
||||
productionIncluded: count(([, v]) => v.dev !== true),
|
||||
developmentOnly: count(([, v]) => v.dev === true),
|
||||
transitive: count((row) => !isDirect(row)),
|
||||
transitiveProduction: count((row) => !isDirect(row) && row[1].dev !== true),
|
||||
transitiveDevelopment: count((row) => !isDirect(row) && row[1].dev === true),
|
||||
});
|
||||
}
|
||||
NODE
|
||||
```
|
||||
|
||||
install script 候補は次で再現できる。
|
||||
|
||||
```bash
|
||||
node -e 'for (const f of ["package-lock.json","ui/package-lock.json"]) { const l=require("./"+f); console.log(f,Object.entries(l.packages).filter(([p,v])=>p&&v.hasInstallScript).map(([p,v])=>({path:p,version:v.version,dev:!!v.dev,optional:!!v.optional}))) }'
|
||||
```
|
||||
|
||||
語彙候補は、同じ拡張子・除外条件で次のように再抽出できる。
|
||||
|
||||
```bash
|
||||
rg -l -i 'telemetr|analytics?|sentry|datadog|new\s?relic|mixpanel|amplitude|posthog|rollbar|bugsnag|phone.?home|usage.?report|update.?check|license.?check|opentelemetry' node_modules ui/node_modules --glob '*.js' --glob '*.cjs' --glob '*.mjs' --glob '*.json' --glob '!**/*.map'
|
||||
```
|
||||
|
||||
この調査で使った機械集計結果は `/tmp/dependency-egress-inventory.json`、語彙走査結果は `/tmp/dependency-egress-scan.json` に残した。これらは一時成果物であり、再現の基準は上記 lockfile hash と手順である。
|
||||
|
||||
## 限界と次段
|
||||
|
||||
- lockfile の全 records は集計したが、全依存の全関数を手作業で監査してはいない。
|
||||
- root 83 records と UI 28 records は、この OS/CPU では展開されない optional package だった。これらは lockfile の package metadata と名前だけを棚卸しし、実コードは走査していない。
|
||||
- keyword scan は難読化、動的 URL 組み立て、語彙を含まない独自収集を検出できない。逆に、多数の false positive を含む。
|
||||
- 通常 telemetry と別の、利用者設定による外部 API 通信、外部コンテンツ取得、悪意ある入力を使った egress はこの文書の結論に含めない。
|
||||
- 強い保証が必要な次段では、隔離ネットワーク上で主要 UI 操作と server workload を実行し、DNS・HTTP(S)・WebSocket を process/browser 単位で記録する。未展開 optional package は対象 OS ごとに `--ignore-scripts` で展開して静的確認し、必要な lifecycle script だけを別の隔離環境で観測する。
|
||||
@@ -0,0 +1,58 @@
|
||||
# 通常通信の観測とブラウザの送信先制限
|
||||
|
||||
## 有効化
|
||||
|
||||
管理者用 Settings → Network → 通信の観測・制限から、既存の config.yaml へ `egress` を保存できる。未設定の既定値は観測・CSPとも無効。UIからの保存は設定変更通知で直ちにサーバーへ反映され、サーバー再起動は不要。ブラウザのCSPは次の文書レスポンスから適用されるため、既存の画面・タブは再読み込みが必要。
|
||||
|
||||
UIでの変更を禁止したい環境では、管理者だけが書き換えられる JSON ファイルを用意し、`AAO_EGRESS_POLICY=/etc/maestro/egress.json` を起動環境へ指定する。変更には再起動が必要。指定した場合は config.yaml より優先し、UIとAPIで通信設定の変更をロックする。外部ファイル未指定なら保存済みのUI設定を使い、明示したファイルが存在しない・不正・64 KiB 超ならサービス起動前に失敗する。ワーカーと Gateway の標準エントリーポイント、および旧 `dist/index.js` で読み込む。
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"observeNode": true,
|
||||
"browser": {
|
||||
"mode": "report-only",
|
||||
"connectOrigins": [],
|
||||
"imageOrigins": ["https://tile.openstreetmap.org"],
|
||||
"frameOrigins": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
用途ごとの配列は最大128件、各要素はスキーム・ホスト・ポートのみ。同じサイトの通信は既定で許可される。接続先には必要に応じて `wss://...` も指定する。空配列は追加許可なし。パス、資格情報、ワイルドカード、未知の設定項目を拒否する。この外部ファイルはアプリの管理画面や設定変更APIから編集できない場所へ配置する。外部ロック中でも、無関係な設定の保存は可能。同値の通信設定を含む全体保存では、外部ファイルや保存済みのegress値を書き換えない。
|
||||
|
||||
ブラウザの `off` は無効、`report-only` は報告のみ、`enforce` は遮断。まず起動・待機・使用する全機能を report-only で検査し、必要な宛先をレビューしてから enforce へ切り替える。特に地図タイル、外部アバター、Markdown画像、直接モデル一覧を取得する推論サーバー、埋め込みページを確認する。既存のファイル閲覧用 `sandbox` CSP は追加ポリシーとして保持される。
|
||||
|
||||
## 記録と限界
|
||||
|
||||
Node は `diagnostics_channel` の HTTP client、Undici、TCP socket チャンネルを観測する。HTTP は論理的な origin、TCP は接続成功時のリモートIP・ポート。WebSocket の handshake はHTTPとして数える。HTTP と TCP は同一リクエストの別層なので合算しない。Node 22 の実通信で検証している。UIから観測を無効にすると購読・定期集計を停止し、有効に戻すと新しい集計を開始する。
|
||||
|
||||
`[egress]` に新規の接続先、`[egress-summary]` に60秒間隔の累積件数を標準ログへ出力する。既定のログレベル info が必要。保持上限は256件で、上限以降の新規宛先の件数を `overflow` に計上する。再起動で集計は消える。本文、Cookie、Authorization、URLのパス・クエリ・フラグメントは収集しない。originのホスト名・IPも機密となり得るため、ログのアクセス権と保存期間を別途管理する。
|
||||
|
||||
**この観測はサーバー側の遮断でも、全通信の証明でもない。** 子プロセス、ネイティブ通信、UDP、DNS問い合わせ、接続失敗したTCPなどは網羅しない。詳細は[サーバーのネットワーク境界](./server-egress-boundary.md)を参照する。エージェントが生成したコードや子プロセスにも制限を効かせるにはOS側の境界が必要。
|
||||
|
||||
`[security:csp]` は最大256種類の違反の初回だけを記録する(同じ違反の再送や集計上限以降はログに追加しない)。受信は1分1000要求、本文16 KiBを上限とし、圧縮本文を拒否する。
|
||||
|
||||
ブラウザのCSP違反報告は未認証・未検証のクライアント入力であり、監査証跡や自動許可判断に使わない。許可した外部iframe内部の通信、利用者による別ページへの移動、ブラウザ拡張機能、既に開いている旧ポリシーのページはこのCSPだけでは管理できない。適用後は画面を再読み込みする。同一originサーバーのプロキシAPIや許可済みサービスを通じた送信も、CSPだけでは排除できない。
|
||||
|
||||
## 回帰検証
|
||||
|
||||
```sh
|
||||
npm run build:server
|
||||
npm run build:ui
|
||||
npm run security:egress
|
||||
npm run security:egress-settings
|
||||
npm run security:ui-assets -- --csp
|
||||
```
|
||||
|
||||
Playwright Chromiumが必要。検査は一時DB・一時設定・最小限の子プロセス環境で実ワーカーを起動し、ループバック上の偽LLM・許可先・拒否先だけを使う。report-only/enforceでHTTP・画像・WebSocketを検査し、同一originのsandbox iframe内の画像遮断と不正ポリシーによる起動拒否も検証し、enforceで65秒待機して定期観測を確認する。本番の接続設定や資格情報は継承しない。さらに同じCSPを実UI部品の検査に適用し、フォント・Monaco編集保存・worker・Leaflet描画と未許可CDNの遮断を確認する。設定画面からの保存・即時反映・再起動後の保持・外部ポリシーのロックも検査する。CIでも実行する。
|
||||
|
||||
この検査の成功は、テストで通した起動・画面・待機経路の結果であり、全依存ライブラリの全機能の無通信を保証しない。OS境界の遮断試験は別途、実際の配備先で実施する。
|
||||
|
||||
## 参照
|
||||
|
||||
- [Node.js diagnostics_channel](https://nodejs.org/api/diagnostics_channel.html)
|
||||
- [Undici diagnostics channels](https://github.com/nodejs/undici/blob/main/docs/docs/api/DiagnosticsChannel.md)
|
||||
- [CSP: 複数ポリシーの適用](https://www.w3.org/TR/CSP3/#multiple-policies)
|
||||
|
||||
HTTPヘッダーの送信直前にCSPを合成するため、既存の推移的依存 `on-headers` を直接依存として明示した。`@types/on-headers` は開発時の型定義のみ。
|
||||
@@ -0,0 +1,131 @@
|
||||
# サーバー外向き通信の境界制御
|
||||
|
||||
更新日: 2026-09-08
|
||||
|
||||
## 適用状態
|
||||
|
||||
このリポジトリは、Linux systemd と既存 Squid を使う配布用 template を提供する。
|
||||
|
||||
- [`maestro-egress.service.example`](../../deploy/egress/systemd/maestro-egress.service.example) は、MAESTRO service cgroupへ適用する IP 制限の staging 用設計例である。対象OSでの強制を検証するまで本番へ配備しない。
|
||||
- [`squid.conf`](../../deploy/egress/squid.conf) は許可した hostname と port の組だけを転送する。
|
||||
- [`check-egress-boundary.mjs`](../../scripts/security/check-egress-boundary.mjs) は外部へ接続せず、template と proxy policy の回帰検査を行う。
|
||||
|
||||
**ネットワーク制御の設計例の配布までで、実 OS へ隔離を適用した試験は未実施である。** 調査環境には Docker CLI、Squid、nftables、tcpdump が無いため、Docker Compose 構成や実 host の cgroup-BPF 動作を完成扱いしない。template を install・enable する前に、対象 host の staging 環境で後述の確認が必要である。
|
||||
|
||||
## 制御モデル
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
Client[利用者] --> Ingress[専用 ingress proxy<br/>source 127.0.0.3]
|
||||
Ingress --> App[MAESTRO<br/>127.0.0.4]
|
||||
App -->|HTTP_PROXY / HTTPS_PROXY| Proxy[Squid<br/>127.0.0.2:3128]
|
||||
Proxy -->|exact hostname + port のみ| Approved[承認済み接続先]
|
||||
App -. cgroup で拒否する設計・要実測 .-> Direct[直接 HTTP / WS / DNS / raw TCP]
|
||||
```
|
||||
|
||||
設計例は systemd の `IPAddressDeny=any` と次の 2 address の `IPAddressAllow` を組み合わせる。kernel、systemd、container managerによってcgroup-BPF対応が異なるため、unit記述だけでは強制を証明しない。
|
||||
|
||||
| address | 用途 | 条件 |
|
||||
|---|---|---|
|
||||
| `127.0.0.2/32` | 外向き Squid | この address には Squid 以外を bind しない。systemd の許可は port を限定できない。 |
|
||||
| `127.0.0.3/32` | ingress proxy の接続元 | listening service を置かず、ingress proxy が MAESTRO 向け接続の source address としてだけ使う。 |
|
||||
|
||||
`127.0.0.3` はプロセスの identity や認証を保証せず、同一 host 上の別プロセスも source address として bind できる。この設計例は host 上の local user と process を信頼境界内とし、信頼できない local workload がある場合は network namespace や host firewall/cgroup identity による追加分離が必要である。
|
||||
|
||||
MAESTRO は `127.0.0.4` で listen する。systemd の IP filter は outbound の destination と inbound の source の両方へ同じ allow list を使う。このため、通常の `127.0.0.1` client や LAN subnet を `IPAddressAllow` へ足すと、その address/subnet は同時に外向き通信先としても許可される。`localhost`、`127.0.0.0/8`、`::1/128`、利用者 LAN 全体を便宜的に許可しない。ingress proxy に専用 source address を設定できない環境では、この template をそのまま有効化できない。
|
||||
|
||||
template に IPv6 の許可値はない。これは対象 host で IP policy が有効な場合にだけIPv6拒否になる。IPv6を使う場合も専用proxyとingress sourceの個別 `/128` だけを追加し、別canaryで実測する。
|
||||
|
||||
## Squid policy
|
||||
|
||||
初期設定の `allowed.egress.invalid:443` は公開 DNS で解決しない canary であり、意図せず外部へ出ない。運用時は次の 3 行を接続先ごとに作り、hostname と port を置き換える。
|
||||
|
||||
```squidconf
|
||||
acl provider_a_host dstdomain -n api.provider.example
|
||||
acl provider_a_port port 443
|
||||
http_access allow provider_a_host provider_a_port
|
||||
```
|
||||
|
||||
exact hostname として扱うため、先頭に `.` を付けない。`.example.com` は subdomain も許可するため、この policy の既定には使わない。`dstdomain -n` はIP literalに対するreverse DNS検索を行わない。IP literalはhostname ACLに一致せず、末尾の `http_access deny all` で拒否される。許可 hostname が DNS rebinding で別 IP を返す可能性は proxy 側の名前解決・接続 policy として別途評価する。private、link-local、metadata CIDR へ解決した結果を禁止できる Squid ACL も staging で追加確認する。
|
||||
|
||||
`access_log none` と `cache_store_log none` が既定で、URL path、query、Authorization header、body、DNS query name を保存しない。DNS query name 自体に秘密が埋め込まれる場合があるため、packet capture や resolver の query log は既定の観測手段にしない。
|
||||
|
||||
設定画面で LLM、MCP、OAuth、webhook などの endpoint を追加しても、Squid policy は自動更新しない。アプリ自身が編集できる設定から境界の許可を自動生成すると、アプリ権限を得た主体が境界も広げられるためである。手順は「アプリ設定を保存する → default deny を確認する → 別権限の運用者が hostname+port を追加する → Squid を検証・reloadする」とする。
|
||||
|
||||
## 導入手順
|
||||
|
||||
1. `maestro` 専用 user と、MAESTRO、ingress proxy、Squid が専用に使う address を staging host で用意する。
|
||||
2. Squid template の `.invalid` ACL を、承認済み exact hostname+port に置き換える。
|
||||
3. `squid -k parse -f <path>` で構文を確認し、Squid を `127.0.0.2:3128` に bind する。
|
||||
4. systemd unit の `/opt/maestro`、user/group、EnvironmentFile を実環境へ合わせる。
|
||||
5. ingress proxy の MAESTRO 向け source address を `127.0.0.3` に固定する。MAESTRO は `127.0.0.4` で listen する。
|
||||
6. `systemd-analyze verify <unit>` に加え、service cgroup内から独立したlistening canaryへ接続し、IPv4/IPv6/direct DNS/raw TCP/子processが拒否されることを確認してからenableする。接続拒否やtimeoutだけでBPFの有効性を断定せず、systemd journalとkernel側のpolicy attach状態も照合する。少数のcanary成功だけを全経路の証明にしない。
|
||||
|
||||
unit は [`server.sh`](../../scripts/server.sh#L123-L147) を呼ばず、migration 後に `dist/main.js` を直接起動する。`server.sh` は起動ごとに `prepare.sh` と build を実行し、資産不足時には download が発生し得るためである。dependency準備とbuildは管理者が runtime service の外で明示的に行う。`AAO_MODE=worker` 用 template であり、gateway mode は別 unit と policy を作る。
|
||||
|
||||
`.env` 相当は `/etc/maestro/maestro.env` から読むが、内容をaudit outputに出さない。`EnvironmentFile`は`Environment=`を上書きできるため、proxy変数・`HOST`・modeは`ExecStart`の`/usr/bin/env`引数で最終指定し、小文字proxy変数、`ALL_PROXY`、`NODE_OPTIONS`を先に除去する。`NO_PROXY` に外部 hostname、LAN、localhost を追加すると proxy を迂回するため空のままにする。
|
||||
|
||||
## 秘密を保存しない観測
|
||||
|
||||
unit の `IPAccounting=yes` では、service と子プロセスを合わせた ingress/egress packet・byte の集計を得られる。
|
||||
|
||||
```bash
|
||||
systemctl show maestro-egress.service \
|
||||
-p IPIngressBytes -p IPIngressPackets \
|
||||
-p IPEgressBytes -p IPEgressPackets
|
||||
```
|
||||
|
||||
この集計は URL、header、body、DNS name、API key を記録しない。一方、宛先や protocol は判別できず、拒否された試行の完全な台帳にもならない。
|
||||
|
||||
接続単位の短期観測が必要な場合、Squid template にある `maestro_meta` logformat を staging だけで有効にできる。記録項目は時刻、HTTP method、接続先 IP/port、status、bytes、時間で、URL/path/query/header/body/DNS name は含めない。file mode、retention、閲覧権限を先に決める。hostname が必要な場合も平文保存せず、運用側 collector で session ごとの鍵付き HMAC と policy rule ID に置き換える。
|
||||
|
||||
proxy経由のHTTP(S)はSquidが名前解決する。IP policyが有効ならAF_INET/AF_INET6でresolverへ直接送る通信もallow list外になるが、`AF_UNIX`はアプリ動作のため許可しており、`nss-resolve`、system bus、host socket上の代理serviceへ委譲する経路はこのtemplateだけでは遮断を保証できない。resolverのquery logは有効にせず、必要ならquery数とallow/deny数だけを集計する。
|
||||
|
||||
## 現在の通信との互換性
|
||||
|
||||
Node.js 要件は [`package.json`](../../package.json#L12-L14) の `>=22` で、調査環境の Node 22.22.3 はexperimentalな `--use-env-proxy` を持つ。unit例はmain Node processだけにこのflagと`HTTP_PROXY` / `HTTPS_PROXY`を設定する。`NODE_OPTIONS`は子processへ強制継承しない。これは global `fetch` を proxy に通す補助策であり、境界そのものではない。
|
||||
|
||||
| 経路 | 境界有効時の見込み | 根拠・対応 |
|
||||
|---|---|---|
|
||||
| LLM request と backend probe の global `fetch` | Squid 経由で許可可能 | LLM は [`openai-compat.ts`](../../src/llm/openai-compat.ts#L1277-L1283)、probe は [`backend-probes.ts`](../../src/engine/backend-probes.ts#L410-L448) で global fetch を使う。 |
|
||||
| `node:http` / `node:https` を直接使う依存 | OS 境界で拒否され得る | env proxyだけでは全 transport を覆わない。transport固有agentを確認してから許可する。 |
|
||||
| Discord REST / resident WebSocket | 現状は拒否され得る | active binding は起動時にREST照会後、resident WSへ接続する。[`discord-gateway.ts`](../../src/bridge/chat/discord-gateway.ts#L189-L210) [`discord-gateway.ts`](../../src/bridge/chat/discord-gateway.ts#L214-L266) |
|
||||
| Web Push | 現状は拒否され得る | `web-push` が独自 HTTPS transport を使うため、proxy対応を個別検証する。[`push-service.ts`](../../src/push-service.ts#L206-L212) |
|
||||
| MCP | transportごとに確認が必要 | 接続はSDK transportへ委譲される。[`client-factory.ts`](../../src/mcp/client-factory.ts#L34-L65) |
|
||||
| SSRF strict fetch | 現状は拒否され得る | DNS pinning用の明示的 Undici Agentを渡すため、global dispatcherを使わない。[`ssrf-strict.ts`](../../src/net/ssrf-strict.ts#L128-L170) |
|
||||
| SSH | 拒否 | raw TCPであり、HTTP proxy対象外。専用bastion/proxy設計なしにIP allowを追加しない。 |
|
||||
| Chromium / user script / LibreOffice / Python helper | systemd IP policyが実効なら同じservice cgroupのIP filter対象 | Chromiumにproxyを渡す追加対応なしでは外部閲覧が失敗する。AF_UNIX経由の代理serviceや別cgroupへの処理委譲は別途監査する。子プロセス例は [`script-runner.ts`](../../src/user-folder/script-runner.ts#L1-L16)、[`office-preview.ts`](../../src/bridge/office-preview.ts#L537-L561)。 |
|
||||
| Bash sandbox | より内側の別制御 | `safety.bash_allow_network` はbwrap内だけを切り替える。[`sandbox.ts`](../../src/engine/tools/sandbox.ts#L81-L87) 外側のservice境界を緩めない。 |
|
||||
|
||||
未設定の fresh config では外部接続が無いことを期待する。LLM worker を設定すると、backend registry は起動し、UI未購読時は既定30秒、購読中は5秒周期でprobeする。[`server.ts`](../../src/bridge/server.ts#L674-L687) [`backend-status-registry.ts`](../../src/engine/backend-status-registry.ts#L176-L190) 許可 policy に worker endpoint が無ければoffline表示になる。この失敗は境界が機能している結果として運用監視で区別する。
|
||||
|
||||
## オフライン回帰検査
|
||||
|
||||
```bash
|
||||
node scripts/security/check-egress-boundary.mjs
|
||||
```
|
||||
|
||||
検査はすべて loopback と `.invalid` hostnameを使い、外部本番接続を行わない。
|
||||
|
||||
- systemd template が `IPAddressDeny=any`、個別 `/32`、空の `NO_PROXY` を維持すること
|
||||
- Squid template が exact host+port、末尾 default deny、access log無効を維持すること
|
||||
- 最小環境の実Node 22 childを`--use-env-proxy`で起動し、global `fetch`がlocal proxyへ実際にCONNECTし、proxyが見た`allowed.egress.invalid:443`を許可、`denied.egress.invalid:443`を拒否すること
|
||||
- local policy model がHTTP、CONNECT、WebSocket Upgradeをexact host+portでallow/denyすること
|
||||
- secret canaryが保存するmetadataと最終出力に含まれないこと
|
||||
- `node:http`、raw TCP、spawn child、IPv4、IPv6、直接DNSがenv proxyだけでは止まらないこと
|
||||
|
||||
実Node childには親processのproxy設定、`NODE_OPTIONS`、資格情報を継承せず、proxyはtunnel内HTTPにもloopbackで応答して外部へ転送しない。最後の項目は、OS境界を適用した検査ではない。HTTP proxy変数だけでは不足することをローカルcanaryで再現し、systemd等のOS境界が必要な理由を固定する検査である。実OSでの強制を証明しない。
|
||||
|
||||
## 実環境で残る検証
|
||||
|
||||
staging host では次を完了するまで本番適用済みと判定しない。
|
||||
|
||||
- kernel/systemdが `IPAddressDeny` / `IPAddressAllow` を実際にload・attachし、service子processのAF_INET/AF_INET6 socketにも作用すること
|
||||
- HTTP fetchの許可先成功と未許可先拒否
|
||||
- IPv4 literal、IPv6 literal、AF_INET/AF_INET6の直接DNS、raw TCP、spawn childの拒否。AF_UNIX代理経路は別に確認
|
||||
- proxy停止時にfail closedとなり、直接接続へfallbackしないこと
|
||||
- ingress proxyからUI/APIへ到達でき、他sourceからは到達できないこと
|
||||
- LLM probe・job、OAuth、MCP、Discord、Web Push、webhook、Chromiumなど、利用する機能ごとの互換性
|
||||
- audit metadataへAPI key、cookie、Authorization、URL query、request/response body、DNS query nameが入らないこと
|
||||
|
||||
Docker Compose版は別段階とする。現行 [`docker-compose.yml`](../../docker-compose.yml#L25-L39) は `host.docker.internal:host-gateway` と通常の外部routeを持つため、env proxyを足すだけでは境界にならない。実装する場合は MAESTROを `internal: true` networkだけへ置き、egress proxyだけをinternal/external両networkへ接続し、`extra_hosts`を削除した上でDocker環境のDNS迂回を実測する。
|
||||
Reference in New Issue
Block a user