[v0.1/SSH Review] retarget 経由の env_base 漏洩とリモート spawn の env 継承 #43

Closed
opened 2026-09-03 16:30:52 +09:00 by natsukium · 1 comment
Owner

現状

  • SpawnArgs.env_base は「dial するプロセスが直前に capture した環境」を Carrier::Local のときのみ送る(env_base::fill_for_carrier)。Carrier::Ssh では None にして relay の FRLY block に委譲。
  • relay の capture_environment()std::env::vars_os() 全量(4096 entries / 1MiB cap、超えると None に degrade)。PATH / SSH_AUTH_SOCK 以外に AWS_SECRET_ACCESS_KEY / GITHUB_TOKEN 等の秘密も含まれる。SSH 自体は暗号化されるが、送り先は remote daemon のメモリと、そこから spawn される子の環境。
  • felis window retarget --host remote -- <cmd> / felis ssh remote -- <cmd> で新規 remote session を作るとき、dial する window のローカル環境が remote daemon に送られる。ローカルの秘密が remote host に漏洩し得る。
  • SpawnArgs.env(明示的 --env KEY=VAL)は SpawnArgs.env_base の上に overlay されるが、env_base 自体のフィルタリングは felis-pty の denylist のみ。allowlist はない。

問い(1.0 でしか変えられない)

env_base / FRLY の「全環境を送る」設計をこのまま凍結するか? 最小限の allowlist に絞るか?

破壊的変更案

  • 案A: env_base / FRLY を allowlist に絞る。 PATH / HOME / SHELL / TERM / LANG / LC_* / SSH_AUTH_SOCK のみを送り、他は送らない。SpawnArgs.env_base の cap(4096/1MiB)はそのままに、送るキー集合を固定。docs/reference/ipc.md "Session (kind = 4)" の env_base 記述を allowlist として normative に。
  • 案B: 明示的 --env 以外は送らない。 fill_for_carrier / capture_environment をやめ、env_base は常に None。remote spawn は常に remote daemon の環境 + 明示的 --env のみ。1.0 で最も安全だが、既存の「ローカルの PATH を引き継ぐ」挙動が失われる。
  • 案C: 現行維持 + 警告。 capture()AWS_*/*_TOKEN/*_SECRET を含むときに tracing::warn(値は出さずキー名のみ)し、docs/reference/ipc.md に「ローカル秘密が remote に送られる」旨を Security Considerations として追記。

判定基準

  • explanation/security-model.md "Process and environment boundary" と reference/ipc.md の env_base / FRLY 記述が、実装の env_base::capture / relay::capture_environment と一致していること。
  • felis --host remote -- env で子の env にローカルの AWS_SECRET_ACCESS_KEY が現れないこと(allowlist or no-send)。
  • felis sessions spawn --env FOO=bar の明示的 overlay は常に最優先で残ること(REQ-912)。

対象ファイル

  • crates/felis-client-core/src/env_base.rs, crates/felis-daemon/src/relay.rs (capture_environment), crates/felis-protocol/src/messages/session.rs (SpawnArgs), crates/felis-protocol/proto/felis.proto (EnvBase), crates/felis-transport/src/preface.rs (CarrierBlock), docs/reference/ipc.md, docs/explanation/security-model.md, docs/explanation/architecture/session-lifecycle.md

cc @natsukium

## 現状 - `SpawnArgs.env_base` は「dial するプロセスが直前に capture した環境」を `Carrier::Local` のときのみ送る(`env_base::fill_for_carrier`)。`Carrier::Ssh` では `None` にして relay の `FRLY` block に委譲。 - relay の `capture_environment()` は `std::env::vars_os()` 全量(4096 entries / 1MiB cap、超えると `None` に degrade)。`PATH` / `SSH_AUTH_SOCK` 以外に `AWS_SECRET_ACCESS_KEY` / `GITHUB_TOKEN` 等の秘密も含まれる。SSH 自体は暗号化されるが、送り先は remote daemon のメモリと、そこから spawn される子の環境。 - `felis window retarget --host remote -- <cmd>` / `felis ssh remote -- <cmd>` で新規 remote session を作るとき、dial する window のローカル環境が remote daemon に送られる。ローカルの秘密が remote host に漏洩し得る。 - `SpawnArgs.env`(明示的 `--env KEY=VAL`)は `SpawnArgs.env_base` の上に overlay されるが、`env_base` 自体のフィルタリングは `felis-pty` の denylist のみ。allowlist はない。 ## 問い(1.0 でしか変えられない) `env_base` / `FRLY` の「全環境を送る」設計をこのまま凍結するか? 最小限の allowlist に絞るか? ## 破壊的変更案 - **案A: `env_base` / `FRLY` を allowlist に絞る。** `PATH` / `HOME` / `SHELL` / `TERM` / `LANG` / `LC_*` / `SSH_AUTH_SOCK` のみを送り、他は送らない。`SpawnArgs.env_base` の cap(4096/1MiB)はそのままに、送るキー集合を固定。`docs/reference/ipc.md` "Session (kind = 4)" の env_base 記述を allowlist として normative に。 - **案B: 明示的 `--env` 以外は送らない。** `fill_for_carrier` / `capture_environment` をやめ、`env_base` は常に `None`。remote spawn は常に remote daemon の環境 + 明示的 `--env` のみ。1.0 で最も安全だが、既存の「ローカルの `PATH` を引き継ぐ」挙動が失われる。 - **案C: 現行維持 + 警告。** `capture()` が `AWS_*`/`*_TOKEN`/`*_SECRET` を含むときに `tracing::warn`(値は出さずキー名のみ)し、`docs/reference/ipc.md` に「ローカル秘密が remote に送られる」旨を Security Considerations として追記。 ## 判定基準 - `explanation/security-model.md` "Process and environment boundary" と `reference/ipc.md` の env_base / FRLY 記述が、実装の `env_base::capture` / `relay::capture_environment` と一致していること。 - `felis --host remote -- env` で子の `env` にローカルの `AWS_SECRET_ACCESS_KEY` が現れないこと(allowlist or no-send)。 - `felis sessions spawn --env FOO=bar` の明示的 overlay は常に最優先で残ること(REQ-912)。 ## 対象ファイル - `crates/felis-client-core/src/env_base.rs`, `crates/felis-daemon/src/relay.rs` (`capture_environment`), `crates/felis-protocol/src/messages/session.rs` (`SpawnArgs`), `crates/felis-protocol/proto/felis.proto` (`EnvBase`), `crates/felis-transport/src/preface.rs` (`CarrierBlock`), `docs/reference/ipc.md`, `docs/explanation/security-model.md`, `docs/explanation/architecture/session-lifecycle.md` cc @natsukium
Author
Owner

Closed because the reported cross-host secret leak is not present. felis-client-core::env_base::fill_for_carrier sets SpawnArgs.env_base only for Carrier::Local; the SSH case sends None. The FRLY block is created by felis-daemon relay after SSH starts the remote command, so relay::capture_environment() captures the remote login environment, not the local window environment. This invariant is also recorded in docs/explanation/security-model.md under “Process and environment boundary” and covered by only_a_local_carrier_carries_an_environment. A local variable reaches the remote only if SSH itself is configured to forward it, which remains SSH policy.

Closed because the reported cross-host secret leak is not present. `felis-client-core::env_base::fill_for_carrier` sets `SpawnArgs.env_base` only for `Carrier::Local`; the SSH case sends `None`. The `FRLY` block is created by `felis-daemon relay` after SSH starts the remote command, so `relay::capture_environment()` captures the remote login environment, not the local window environment. This invariant is also recorded in `docs/explanation/security-model.md` under “Process and environment boundary” and covered by `only_a_local_carrier_carries_an_environment`. A local variable reaches the remote only if SSH itself is configured to forward it, which remains SSH policy.
Sign in to join this conversation.
No description provided.