From ac6e82f8e9863536084c346294b34f97461ebc29 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Mon, 14 Sep 2026 16:16:52 +0900 Subject: [PATCH 1/6] =?UTF-8?q?docs(PLAN54):=20base=20=E3=82=A4=E3=83=A1?= =?UTF-8?q?=E3=83=BC=E3=82=B8=E3=81=AB=20bao=20=E3=82=92=E5=85=A5=E3=82=8C?= =?UTF-8?q?=E3=80=81=E3=82=B3=E3=83=B3=E3=83=86=E3=83=8A=E3=81=8B=E3=82=89?= =?UTF-8?q?=E6=A9=9F=E5=AF=86=E3=82=92=E8=AA=AD=E3=81=BF=E6=9B=B8=E3=81=8D?= =?UTF-8?q?=E3=81=99=E3=82=8B=E8=A6=81=E6=B1=82=E4=BB=95=E6=A7=98=E3=81=A8?= =?UTF-8?q?=E8=A8=AD=E8=A8=88=20(#169)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01S9okWVz1S7VGUsCQWVMhb3 --- issues/PLAN54_bao-in-container-design.md | 270 +++++++++++++++++++++++ issues/PLAN54_bao-in-container.md | 167 ++++++++++++++ 2 files changed, 437 insertions(+) create mode 100644 issues/PLAN54_bao-in-container-design.md create mode 100644 issues/PLAN54_bao-in-container.md diff --git a/issues/PLAN54_bao-in-container-design.md b/issues/PLAN54_bao-in-container-design.md new file mode 100644 index 00000000..080b1352 --- /dev/null +++ b/issues/PLAN54_bao-in-container-design.md @@ -0,0 +1,270 @@ +# #169: base イメージに `bao` を入れ、起動中のコンテナから機密を取得・変更できるようにする(設計) + +要求と受け入れ条件は `issues/PLAN54_bao-in-container.md` にある。この文書は「どう作るか」だけを扱う。 + +作るものは 3 つである。base イメージの `bao` 2.6.2、`devbase up` がコンテナへ渡す +`BAO_ADDR` と `~/.vault-token`、token を取り直す `devbase env token`。コンテナに置く資格情報は +1 時間で切れる token だけで、`secret_id` はホストから出ない(決定 1・2)。 + +## 機能一覧 + +| # | 機能 | 誰が使うか | +| --- | --- | --- | +| F1 | dev コンテナの中で `bao` 2.6.2 が使える | 開発者、管理者(carmo-cdk の管理スクリプト) | +| F2 | `devbase up` が、backend が `openbao` のとき、コンテナへ接続先と token を渡す | 開発者(意識せずに使う) | +| F3 | `devbase env token` で、起動中のコンテナの token を再起動せずに取り直す | 開発者(token が切れたとき) | +| F4 | コンテナの中から自分の機密を読む・足す・変える・消す手順の案内 | 開発者 | + +## 構成要素 + +| 要素 | 責務 | +| --- | --- | +| `containers/base/Dockerfile`(変える) | `bao` の tar.gz を取得し、`checksums.txt` で検証して `/usr/local/bin/bao` へ置く。版は `ARG BAO_VERSION` | +| `env/openbao.py` `OpenBaoBackend.issue_token()`(足す) | 現在の token を返す。無い・期限が近ければログインし直す。既存の `_ensure_token` を公開する薄い入口 | +| `env/container_token.py`(新設) | token をコンテナへ届ける。`docker exec` で `~/.vault-token`(`0600`)へ書く。対象のコンテナ名の解決と、replica ごとの繰り返しを持つ | +| `commands/container.py` `_push_bao_token()`(足す) | `up` の [5/6] の後に呼ぶ。backend が `openbao` でなければ何もしない。失敗しても `up` を倒さない(`_apply_window_titles` と同じ扱い) | +| `commands/container.py` `_generate_compose_for()` の `dev_environment`(変える) | backend が `openbao` のとき `BAO_ADDR=` を dev サービスの `environment` に足す(値はリテラル。機密ではない) | +| `commands/env.py` `cmd_env_token()`(足す) | `devbase env token [--print]`。既定は現在地のプロジェクトの起動中の dev コンテナへ届ける。`--print` は標準出力へ token だけを出す | +| `cli.py` の parser(変える) | `env token` のサブコマンドと `--print` | +| `docs/user/env-backend.md`(変える) | 「コンテナの中から `bao` を使う」の節(F4) | +| `tests/containers/test_base_dockerfile_bao.py`(新設) | Dockerfile の `bao` 導入行を固定する(版・両アーキテクチャ・検証) | +| `tests/env/test_container_token.py`(新設) | `docker exec` の呼び出しの形(コマンド・stdin・umask)と、replica の繰り返し | +| `tests/commands/test_container_bao.py`(新設) | `up` が `BAO_ADDR` を足す/足さない、`_push_bao_token` の要否 | +| `tests/commands/test_env_token.py`(新設) | `env token` の `--print` と既定の経路、backend が `openbao` でないときの失敗 | + +構成要素の関係: + +```mermaid +graph LR + subgraph host [ホスト: devbase] + UP[commands/container.cmd_up] + TOKCMD[commands/env.cmd_env_token] + PUSH[env/container_token.push] + OB[env/openbao.OpenBaoBackend] + CFG[secrets/backend.yml] + BOOT[secrets/bootstrap.env.age] + end + subgraph container [dev コンテナ] + TF["~/.vault-token (0600)"] + ENVV[BAO_ADDR] + BAO[bao CLI 2.6.2] + end + SRV[(OpenBao サーバ)] + UP -->|environment に BAO_ADDR| ENVV + UP --> PUSH + TOKCMD --> PUSH + PUSH -->|issue_token| OB + OB -->|approle login| SRV + OB --> CFG + OB --> BOOT + PUSH -->|docker exec: cat > ~/.vault-token| TF + BAO --> TF + BAO --> ENVV + BAO -->|kv get / put / patch| SRV +``` + +システムの文脈と配置は上の図の `host` / `container` / `SRV` の 3 つである。変更前と同じで、 +`docs/specifications/secret-backend.md`「構成要素」が持つ。この変更が足す辺は 2 本である。 +コンテナからサーバへ向かう辺(`bao` → `SRV`)と、ホストからコンテナへ token を届ける辺。 + +## 構造 + +```mermaid +classDiagram + class OpenBaoBackend { + +login() + +issue_token() str + -_ensure_token() str + -_token: str + -_token_expires_at: float + } + class ContainerTokenPusher { + +push(container_names: list, token: str) list + -_write_token_file(container_name, token) bool + } + class SecretStore { + +backend_name: str + +config: BackendConfig + -_selected_backend() SecretBackend + } + ContainerTokenPusher ..> OpenBaoBackend : issue_token() + ContainerTokenPusher ..> SecretStore : backend が openbao か +``` + +`issue_token()` は `_ensure_token()` をそのまま返す。ログイン失敗の例外(`SecretAuthError` / +`SecretUnreachableError`)はそのまま上へ伝える。`push()` は書けたコンテナ名の一覧を返し、 +書けなかったものは警告を 1 行ずつ出す(`up` の後処理では失敗を握り、`env token` では非ゼロで +終える)。 + +## 入出力の契約 + +### コンテナの中の環境 + +| 名前 | 形 | 出所 | いつ | +| --- | --- | --- | --- | +| `BAO_ADDR` | 環境変数。`backend.yml` の `openbao.url` | `_generate_compose_for` の `dev_environment` | `up` のとき。backend が `openbao` のときだけ | +| `~/.vault-token` | ファイル `0600`、token の文字列 1 行(改行なし) | `_push_bao_token` / `env token` | `up` の [5/6] の後と、`env token` を打ったとき | + +`bao` は `BAO_TOKEN` が無いとき `~/.vault-token`(`BAO_TOKEN_PATH` で変更可)を読む。 +`BAO_TOKEN` を環境変数にしないのは、`docker inspect` と子プロセスの環境に残るためである +(決定 1)。 + +### `devbase env token` + +```text +devbase env token [--print] [-p PROJECT] +``` + +| 引数 | 意味 | +| --- | --- | +| (なし) | 現在地のプロジェクト(`-p` で指定可)の起動中の dev コンテナすべての `~/.vault-token` を書き換える | +| `--print` | コンテナへ書かず、token を標準出力へ 1 行で出す。手で貼りたいとき・別の経路のコンテナのため | + +| 状況 | 出力 | 終了コード | +| --- | --- | --- | +| backend が `openbao` でない | 「backend が openbao ではありません」 | 1 | +| ログインが拒まれた(400 / 403) | 既存の `SecretAuthError` の文言 | 1 | +| 到達できない | 既存の `SecretUnreachableError` の文言(**控えは使わない**。token は控えられない) | 1 | +| 起動中の dev コンテナが無い | 「起動中のコンテナがありません: 」 | 1 | +| 一部のコンテナに書けなかった | 書けたもの・書けなかったものを 1 行ずつ | 1 | +| すべて書けた | 書いたコンテナ名を 1 行ずつ(token は出さない) | 0 | + +`env token` は `_NO_SECRET_INJECTION` に入れる(機密の注入は要らず、注入の往復を増やさない)。 + +### `docker exec` の形 + +```text +docker exec -i sh -c 'umask 077 && cat > "$HOME/.vault-token"' + (stdin: token) +``` + +- `-i` で stdin を渡し、引数に token を載せない(`ps` に出さない) +- 既存の `_docker_exec` の経路(`editor/window_title.py`)と同じく `DOCKER_CONTEXT` を継承する + ため、リモートの daemon(PLAN52)でも同じ形で届く +- `$HOME` はコンテナの利用者(`ubuntu`)のもの。`docker exec` の既定の利用者は compose の + `user` 設定に従い、base イメージは `USER ubuntu` で終わる + +## 処理の流れ + +```mermaid +sequenceDiagram + participant U as 利用者 + participant UP as cmd_up + participant ST as SecretStore/OpenBaoBackend + participant D as docker + participant C as dev コンテナ + U->>UP: devbase up + UP->>ST: _inject_secrets(既存。ログイン + GET) + UP->>D: compose up(environment に BAO_ADDR) + D->>C: 起動 + UP->>UP: [5/6] ready を待つ + alt backend が openbao + UP->>ST: issue_token()(同じインスタンス。期限内なら再ログインしない) + UP->>D: docker exec -i sh -c 'umask 077 && cat > ~/.vault-token' + D->>C: ~/.vault-token を書く + end + UP->>UP: [6/6] エディタ + Note over C: 1 時間後に token が切れる + U->>C: bao kv get … → 403 + U->>UP: devbase env token + UP->>ST: issue_token()(新しい token) + UP->>D: docker exec -i … + U->>C: bao kv get … → 200 +``` + +`up` の途中で `issue_token()` が返す token は、`_inject_secrets(required=True)` と同じ +インスタンスのものである。`_run_deploy_pipeline` が `SecretEnv` を作った `SecretStore` を +保持して渡す。`up` の往復は増えない。 + +**#168(PLAN55)が `SecretStore` の引き回しを変える場合は、この呼び出しもそちらの経路に乗せる。** + +| 実装の順序 | `SecretStore` の渡し方 | +| --- | --- | +| PLAN55 の後 | PLAN55 の設計に従う | +| PLAN55 の前 | `_run_deploy_pipeline` の中で `SecretStore` を 1 つ作り、注入と token の両方へ渡す | + +## 非機能の実現方式 + +| 大項目 | 要求の条件 | 実現方式 | 確かめ方 | +| --- | --- | --- | --- | +| セキュリティ | `secret_id` の置き場所を広げない方式を既定とする | コンテナへ渡すのは 1 時間で切れる token だけ。`secret_id` はホストの `bootstrap.env.age` から出ない。token はファイル `0600` に置き、環境変数と compose ファイルに書かない | コンテナ内で `env \| grep -c SECRET_ID` が 0、`.docker-compose.scale.yml` に `hvs.` が無い、`stat -c %a ~/.vault-token` が 600 | +| セキュリティ | `bao` の導入はチェックサムで検証する | `checksums.txt` を同じリリースから取得し、`grep` で対象行を抜いて `sha256sum -c` | 行を改ざんした Dockerfile でビルドが失敗する(テストは Dockerfile の文言で固定) | +| 運用・保守性 | 版の更新が `ARG` 1 行 | `ARG BAO_VERSION=2.6.2` を 1 か所に置き、URL・ファイル名・検証すべてがそれを参照する。tar.gz には `bao` / `CHANGELOG.md` / `LICENSE` / `README.md` の 4 つが入る(v2.6.2、70 MB)ので `tar -xzf - bao` で 1 つだけ取り出す | `grep -c 2.6.2 containers/base/Dockerfile` が 1 | +| 運用・保守性 | token 切れの症状(403)と対処を `docs/user/` に書く | F4 の節に「`permission denied` が出たら `devbase env token`」を書く | 文書を読む | +| システム環境 | amd64 / arm64 の両方でビルドできる | `dpkg --print-architecture` で `amd64` / `arm64` を選ぶ(session-manager-plugin と同じ書き方) | 両アーキテクチャのホストで `devbase build` | +| システム環境 | backend が `openbao` でない端末は影響を受けない | `BAO_ADDR` の付与と `_push_bao_token` は `store.backend_name == 'openbao'` のときだけ | backend が `age` の `up` で生成される compose が変更前と一致するテスト | + +## 決定の記録 + +### 決定 1: コンテナへ渡す資格情報は 1 時間の token だけにし、`~/.vault-token` に置く + +token は AppRole のログインで得るもので 1 時間で切れる。盗まれても被害は 1 時間で、 +`secret_id`(失効させるまで使える)を置くより狭い。ファイルにするのは、環境変数だと +`docker inspect` と全プロセスの環境に残り、`env` を出力するコマンドや子プロセスに漏れる +ためである。`bao` は `~/.vault-token` を既定で読むので、利用者は何も設定しなくてよい。 + +`role_id` / `secret_id` をコンテナへ渡して `bao write auth/approle/login` で取り直す案は採らない。 +取り直しは楽になる。しかしコンテナの中で動く AI CLI と npm のパッケージが、ホストと同じ +資格情報を永続的に持つ形になる。PLAN51 が `secret_id` を age で守った意味が無くなる。 + +### 決定 2: token の取り直しはホストの `devbase env token` が行い、コンテナには資格情報を置かない + +コンテナの中から取り直す手段を持たせるには、コンテナに資格情報(決定 1 で退けた)か、 +人のログイン(OIDC)が要る。OIDC の `bao login -method=oidc role=google` は、ブラウザの戻り先がコンテナの +`localhost:8250` に届く必要がある。これは VS Code のポート転送に依存する。devbase が作る +ものではなく、文書に「別の経路」として書くだけにする。 + +ホストで取り直す形なら、`devbase up` が既に持つ資格情報と経路をそのまま使える。1 時間ごとの +手間は、コンテナの中で `bao` を打つ頻度(キーを足す・値を見るとき)に対して十分に小さい。 +起動時の注入で足りる日常の利用では、取り直しは一度も要らない。 + +### 決定 3: `BAO_ADDR` は環境変数で渡し、`docker exec` で書かない + +接続先は機密ではなく、compose の `environment` にリテラルで書ける(`DEVBASE_ACCOUNT_GROUP` と +同じ扱い)。ファイルにする token と分けることで、token の書き込みに失敗しても `bao` は「token が無い」と +分かる。`BAO_ADDR` まで無いと誤りが「接続先が無い」に変わり、原因が読めない。 + +### 決定 4: token の書き込みは `up` の [5/6] の後に置き、失敗しても `up` を倒さない + +コンテナが ready になる前は `docker exec` が失敗する。`_apply_window_titles` と同じ位置で、 +同じく付随処理として扱う。token が書けなくてもコンテナは起動済みの環境変数で動くため、 +`up` を失敗にすると本来の目的(開発環境の起動)を損なう。失敗は警告で伝え、利用者は +`devbase env token` でやり直せる。 + +### 決定 5: `bao` は tar.gz を `checksums.txt` で検証して入れ、`.deb` は使わない + +`.deb` は依存の解決と postinst(`openbao` システムユーザーの作成、systemd ユニット)を伴い、 +CLI だけが要る base イメージには余計である。tar.gz は `bao` バイナリ 1 つで、配置先を +`/usr/local/bin` に固定できる。署名(`.gpgsig` / `.sigstore.json`)の検証は、base イメージの +他のツール(AWS CLI・gcloud・uv)も行っていないため揃える。チェックサムは同じリリースから +取るので、改ざんに対する防御は「GitHub Releases が一貫している」前提に依る。 + +### 決定 6: 全端末の base イメージに `bao` を入れ、backend で入れ分けない + +管理者の作業(carmo-cdk の `openbao-admin.sh`)は `bao` に依存し、管理者の端末の backend +設定とは無関係である。入れ分けると、backend を `openbao` にしていない管理者の端末で +`bao` が無い。サイズの増分は 1 バイナリ(tar.gz で 70 MB。展開後の実測は実装時に CHANGELOG へ書く)で、 +既に gcloud SDK と Playwright を持つイメージに対して許容する。 + +## テスト設計 + +| 受け入れ条件(PLAN54) | 何で確かめるか | +| --- | --- | +| 1. `bao version` が `OpenBao v2.6.2` | 手動(`devbase build` → `docker run --rm devbase-base:latest bao version`)。amd64 は CI 相当のホスト、arm64 はこの Mac | +| 2. チェックサム不一致でビルドが失敗する | `tests/containers/test_base_dockerfile_bao.py`: Dockerfile に `sha256sum -c` の行と `checksums.txt` の取得があることを固定。実ビルドの失敗は実装時に 1 度手で確かめる | +| 3. コンテナ内 `bao kv get` がホストの `env get --user` と同じ値 | 手動(リリース後テスト。PLAN53 の後の端末で) | +| 4. コンテナ内 `kv patch` がホストの `env get --user` に見える | 手動(同上) | +| 5. `team/global` は読めて書けない | 手動(同上。サーバのポリシーの確認) | +| 6. 1 時間後に `devbase env token` で読める | 手動(同上)。`tests/commands/test_env_token.py` で `issue_token` → `push` の順と対象コンテナを固定 | +| 7. backend が `age` なら `BAO_ADDR` / token が無く compose が同じ | `tests/commands/test_container_bao.py`: `up` の harness で生成 compose の差分 0、`_push_bao_token` が `docker exec` を呼ばない | +| 8. token がログと compose に書かれない | `tests/env/test_container_token.py`: `docker exec` の argv に token が無く stdin にある。`caplog` に token が無い | +| 9. `pytest` / `ruff` / `shellcheck` | `quality-gates` | +| 10. Dockerfile の導入行を固定するテスト | `tests/containers/test_base_dockerfile_bao.py`(版・`amd64` / `arm64` の分岐・`sha256sum -c`) | + +## 未確認のまま残ること + +| 項目 | 内容 | +| --- | --- | +| `bao` が `~/.vault-token` を既定で読むこと | バイナリの文字列(`~/.vault-token` / `BAO_TOKEN_PATH`)と OpenBao の CLI 文書から読んだ。実サーバで確かめるのはリリース後テスト。読まなければ `BAO_TOKEN_PATH` を `environment` で指す | +| 派生イメージが `USER` を変えていないこと | `containers/*/Dockerfile` の 8 本はすべて `ubuntu`(`${USERNAME}`)で終わる(2026-09-14 に数えた)。利用者側の `projects/*/compose.yml` が `user:` を変える場合は `$HOME` が変わり、その場合は `BAO_TOKEN_PATH` で指す運用になる | +| PLAN55(#168)との順序 | 先に入った側に合わせて `SecretStore` の引き回しを決める(処理の流れの節) | diff --git a/issues/PLAN54_bao-in-container.md b/issues/PLAN54_bao-in-container.md new file mode 100644 index 00000000..acd3a65f --- /dev/null +++ b/issues/PLAN54_bao-in-container.md @@ -0,0 +1,167 @@ +# PLAN54: base イメージに `bao` を入れ、起動中のコンテナから機密を取得・変更できるようにする + +- 発端: #169 +- ワークフローモード: `standard` + - 根拠: base イメージ(`containers/base`)と、コンテナへ接続情報を渡す起動経路 + (`lib/devbase`)の 2 領域にまたがる本番の振る舞いの追加。公開インタフェース + (コンテナ内で使える環境変数)が増える +- 閉じる課題: #169 + +## 依頼(原文) + +> base イメージに OpenBao の CLI(`bao`)を入れる。そのうえで、**起動中のコンテナから、再起動せずに機密の変数を取得・変更できる**ようにする。 + +> | # | 内容 | +> | --- | --- | +> | 1 | base イメージに `bao` を入れる。版はサーバと同じ 2.6 系で固定する(2026-09-13 時点の最新は 2.6.2) | +> | 2 | 起動中のコンテナから、再起動せずに自分の機密を取得できる | +> | 3 | 起動中のコンテナから、再起動せずに自分の機密を変更できる(キーの追加・値の変更・キーの削除) | + +> 管理者の作業(利用者の登録、端末の資格情報の発行と失効)も、base コンテナの `bao` で行う前提にする。サーバ側の管理スクリプトは `bao` を使う。 + +コメント(2026-09-14): + +> 着手時は「手元キャッシュとの整合」の論点を、確定仕様の「控えは読み取り専用(set / delete / edit は fetch で現物を読む)」の規則と突き合わせること。 + +## 目的 + +- 起動中の dev コンテナの中で `bao` が使え、再起動せずに自分の機密(`users//…`)を + 読み書きできる +- サーバと同じ 2.6 系の `bao` を、amd64 / arm64 の両方で、検証付きで入れる +- 管理者が base コンテナから `bao` で管理操作(carmo-cdk の `openbao-admin.sh`)を行える + +## 前提 + +- 前提 1: backend が `openbao` でない端末でも base イメージに `bao` を入れる。サーバ側の + 運用リポジトリの管理スクリプトが `bao` に依存するためである。設定で入れ分けると、管理者の + 端末で `bao` が無い状態が起きる。 + 成否の判定: `containers/base/Dockerfile` に `bao` の導入が条件なしで書かれている +- 前提 2: 版は `2.6.2` に固定し、Dockerfile の `ARG` で持つ。GitHub Releases の + `checksums.txt` で検証する(署名の検証はしない。base イメージの他のツールも署名は + 見ていない)。成否の判定: 版を変えるのが `ARG` 1 行で済む +- 前提 3: コンテナの中で `bao` が接続に使うのは `BAO_ADDR`(環境変数)と token の 2 つで、 + `secrets/backend.yml` を読ませない(コンテナは `$DEVBASE_ROOT` を持たない)。 + ~~token も環境変数 `BAO_TOKEN`~~ → token はファイル `~/.vault-token` に置く(2026-09-14、 + 設計の決定 1。環境変数だと `docker inspect` と子プロセスに残るため)。 + 成否の判定: コンテナ内で `env | grep ^BAO_` が `BAO_ADDR` だけを返し、`~/.vault-token` が `0600` で存在する +- 前提 4: token は 1 時間で切れる(サーバの `token_max_ttl=1h`)。起動中のコンテナから + 取り直す手段が要る。**どの資格情報でどう取り直すか**は設計工程で決める(下の未決)。 + 成否の判定: 設計文書の「決定の記録」にこの決定がある +- 前提 5: コンテナの `bao` で書き込んだ変更は、ホスト側の控え(`secrets/cache/`)には + 反映しない。控えは読み取り専用で、`set` / `delete` / `edit` は `fetch` で現物を読む + (`docs/specifications/secret-backend.md`「失敗の種類とキャッシュ」)。次の `devbase up` が + 現物を読めば整合する。古い値になるのは不達時に控えから起動したときだけで、その旨は既存の + 警告が出る。成否の判定: ホスト側のコードにコンテナからの通知を受ける経路が無い +- 前提 6: 起動済みのプロセスの環境変数は変えられない。コンテナの中での「変更」は + OpenBao 側の値の変更を指し、シェルへ読み直す手段は**案内**(`docs/user/`)で示す + (例: `export KEY="$(bao kv get -field=KEY …)"`)。成否の判定: 起動中のプロセスの + 環境を書き換える仕組みを作らない +- 前提 7: backend が `openbao` でない端末(`age` / `plaintext`)では、`BAO_ADDR` / + `BAO_TOKEN` をコンテナへ渡さず、それ以外の振る舞いは変えない(PLAN51 の前提 3) + +## 対象範囲 + +含む: + +- `containers/base/Dockerfile` への `bao` 2.6.2 の導入(amd64 / arm64、checksums.txt で検証) +- backend が `openbao` のとき、`devbase up` がコンテナへ `BAO_ADDR` と有効な `BAO_TOKEN` を渡すこと +- 起動中のコンテナから token を取り直す手段(方式は設計で決める) +- 利用者への案内(`docs/user/env-backend.md`): パスの形、`kv put` と `kv patch` の違い、 + シェルへの読み直し、token の期限と取り直し +- `docs/specifications/secret-backend.md` への確定仕様の追記(`plan-to-spec`) + +含まない: + +- 起動中のプロセスの環境変数を書き換えること +- コンテナからの書き込みをホスト側の控えへ反映すること(前提 5) +- チーム共通(`team/…`)への書き込み(サーバのポリシーが `devbase-team-writer` に限る。 + `bao` はそのまま使えるため devbase 側で作るものは無い) +- `devbase up` の 2 度注入の解消(#168、PLAN55) +- 管理スクリプト(carmo-cdk `bin/openbao-admin.sh`)の変更 + +## 用語 + +| 用語 | 意味 | +| --- | --- | +| `bao` | OpenBao の CLI。`BAO_ADDR` / `BAO_TOKEN` を環境変数から読む | +| token | AppRole ログインで得る `client_token`。TTL 1 時間、延長不可 | +| 端末の資格情報 | AppRole の `role_id` / `secret_id`。ホストの `secrets/bootstrap.env.age` にある | +| 控え | ホスト側 `secrets/cache/` の age 暗号化キャッシュ。読み取り専用 | + +## 受け入れ条件 + +- [ ] `devbase build`(base イメージ)の後、`docker run --rm devbase-base:latest bao version` が + `OpenBao v2.6.2` を含む行を出す。amd64 と arm64 のどちらのホストでも同じ +- [ ] ダウンロードした tar.gz の SHA-256 が `checksums.txt` と一致しないとき、イメージの + ビルドが失敗する(`Dockerfile` の該当行を読み、`sha256sum -c` 相当の検証があること + で判定する) +- [ ] 前提: backend が `openbao` で `devbase env backend test` が通る + 操作: `devbase up` の後に `docker exec bash -lc 'bao kv get -mount=devbase -field= users//global'` + 結果: ホストの `devbase env get --user ` と同じ値を返す +- [ ] 前提: 同上 + 操作: コンテナ内で `bao kv patch -mount=devbase users//global NEW_KEY=v1`、 + 続けてホストで `devbase env get --user NEW_KEY` + 結果: `v1`(ホスト側は控えではなく現物を読む) +- [ ] 前提: 同上 + 操作: コンテナ内で `bao kv get -mount=devbase team/global` と + `bao kv put -mount=devbase team/global X=1` + 結果: 前者は成功、後者は 403 で終了コード 2(devbase 側で何も作らない。サーバの + ポリシーの確認) +- [ ] 前提: コンテナ起動から 1 時間以上経ち、`BAO_TOKEN` が切れている + 操作: 設計で決めた取り直しの手段を実行してから `bao kv get …` + 結果: 再起動せずに値を読める +- [ ] backend が `age` の端末で `devbase up` したとき、コンテナの `env` に `BAO_ADDR` が無く、 + `~/.vault-token` が作られず、生成される `.docker-compose.scale.yml` が変更前と同じ +- [ ] token の値がログ(`devbase --verbose up` の出力)と `.docker-compose.scale.yml` と + `docker inspect` の `Env` に書かれない(~~compose には変数名だけを列挙する~~ → + token は `docker exec` の stdin で渡す。2026-09-14、設計の決定 1) +- [ ] `uv run pytest tests/` が全件通り、`ruff check lib` と `shellcheck` が変更前と同じ結果 +- [ ] `tests/containers/` に `Dockerfile` の `bao` の導入行を固定するテストがある + (版・アーキテクチャ・検証の 3 点) + +## 非機能の条件 + +| 大項目 | 条件 | +| --- | --- | +| セキュリティ | `secret_id` の置き場所を広げない方式を既定とする(設計で判断し、広げる方式を採るなら「決定の記録」に理由を書く)。token は compose ファイルへ書かない。`bao` の導入はチェックサムで検証する | +| 運用・保守性 | 版の更新が `ARG` 1 行。token 切れのときの症状(403)と対処を `docs/user/` に書く | +| システム環境 | amd64 / arm64 の両方でビルドできる。backend が `openbao` でない端末は影響を受けない | + +## 影響 + +| 対象 | 影響 | +| --- | --- | +| 公開インタフェース | コンテナ内の環境変数 `BAO_ADDR` とファイル `~/.vault-token` が増える(backend が `openbao` のときだけ)。ホストのコマンド `devbase env token` が増える | +| データ | スキーマ変更なし。`secrets/backend.yml` の項目が増えるかは設計で決める | +| 既存の振る舞い | base イメージに `bao` が入る(全端末)。`openbao` の端末では `devbase up` がコンテナへ 2 変数を追加で渡す | + +## 検証手段 + +| 項目 | 手段 | +| --- | --- | +| 起動 | `devbase build` → `devbase up ` | +| テスト | `uv run pytest tests/` | +| 静的解析 | `ruff check lib`、`shellcheck --severity=error bin/devbase install.sh`、`python -m compileall -q lib bin` | +| 手動確認 | 利用者の端末(backend `openbao`、PLAN53 の後)で受け入れ条件 3〜6 を実行する。リリース後テストの工程で行う | + +## 前提とする取り決め + +| 項目 | 参照先 / 決めたこと | +| --- | --- | +| プロジェクト構造 | `docs/developer/architecture.md`。base イメージは `containers/base/`、起動経路は `lib/devbase/commands/container.py` と `lib/devbase/project/runtime.py`(`container_env`)、OpenBao の HTTP は `lib/devbase/env/openbao.py` | +| コーディング規約 | `docs/developer/contributing.md`。CI は `compileall` / `ruff` / `shellcheck` | +| テスト戦略 | `tests/containers/` で Dockerfile とエントリポイントの文言を固定、`tests/env/test_openbao.py` の偽サーバで HTTP の往復を固定、`tests/commands/` で `up` が渡す環境変数を固定。実サーバへの接続は手動確認 | + +## 境界 + +| 区分 | 内容 | +| --- | --- | +| 常に行う | 既存テストの実行、`ruff`、`shellcheck`。`bao` の版は `ARG` で持つ | +| 確認してから行う | 端末の `secret_id` をコンテナへ渡す方式の採用。`secrets/backend.yml` の項目の追加。設計 Pull Request のマージ | +| 行わない | 起動中のプロセスの環境変数の書き換え。ホスト側の控えへの反映。管理スクリプトの変更 | + +## 未決 + +| 項目 | 誰が決めるか | 期限 | +| --- | --- | --- | +| ~~token を取り直す手段~~ → 決まった: ホストの `devbase env token` が起動中のコンテナの `~/.vault-token` を書き換える(設計の決定 2。(b) `secret_id` をコンテナへ渡す案と (c) OIDC 案は採らない) | 設計 Pull Request のマージで利用者が承認する | 設計 | From e52f559bb7bba1f757a3e7b842a0538e3aeb48c4 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Mon, 14 Sep 2026 16:33:16 +0900 Subject: [PATCH 2/6] =?UTF-8?q?docs(PLAN54):=20=E3=83=AC=E3=83=93=E3=83=A5?= =?UTF-8?q?=E3=83=BC=E6=8C=87=E6=91=98=E3=81=AB=E5=AF=BE=E5=BF=9C=EF=BC=88?= =?UTF-8?q?=E6=B1=BA=E5=AE=9A=201=20=E3=81=AE=E5=8F=8D=E6=98=A0=E6=BC=8F?= =?UTF-8?q?=E3=82=8C=E3=80=81Pusher=20=E3=81=AE=E8=B2=AC=E5=8B=99=E5=A2=83?= =?UTF-8?q?=E7=95=8C=E3=80=81env=20token=20=E3=81=AE=E5=BC=95=E6=95=B0?= =?UTF-8?q?=E3=80=81=E5=8E=9F=E5=AD=90=E7=9A=84=E3=81=AA=E6=9B=B8=E3=81=8D?= =?UTF-8?q?=E8=BE=BC=E3=81=BF=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 仕様: 前提 7・対象範囲・受け入れ条件 6・影響・用語に残っていた環境変数 `BAO_TOKEN` の記述を、決定 1(token は `~/.vault-token` に置く)に揃える - 設計: token の取得(`issue_token()`)と backend の判定を呼び出し側 (`_push_bao_token` / `cmd_env_token`)に置き、`ContainerTokenPusher` は `docker` だけを知る形にする。クラス図・構成要素の図・シーケンス図を揃える - 設計: `env token` の `-p PROJECT` を落とし、他の `env` サブコマンドと同じく 現在地のディレクトリで対象を決める。`--print` はコンテナを見ない - 設計: `cli.py` の `SUBCMD_MAP` / `_NO_SECRET_INJECTION` への登録を明記 - 設計: `~/.vault-token` は一時ファイルへ書いて `mv -f` で置き換える Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01S9okWVz1S7VGUsCQWVMhb3 --- issues/PLAN54_bao-in-container-design.md | 76 +++++++++++++++++------- issues/PLAN54_bao-in-container.md | 15 +++-- 2 files changed, 65 insertions(+), 26 deletions(-) diff --git a/issues/PLAN54_bao-in-container-design.md b/issues/PLAN54_bao-in-container-design.md index 080b1352..259b632d 100644 --- a/issues/PLAN54_bao-in-container-design.md +++ b/issues/PLAN54_bao-in-container-design.md @@ -21,14 +21,14 @@ | --- | --- | | `containers/base/Dockerfile`(変える) | `bao` の tar.gz を取得し、`checksums.txt` で検証して `/usr/local/bin/bao` へ置く。版は `ARG BAO_VERSION` | | `env/openbao.py` `OpenBaoBackend.issue_token()`(足す) | 現在の token を返す。無い・期限が近ければログインし直す。既存の `_ensure_token` を公開する薄い入口 | -| `env/container_token.py`(新設) | token をコンテナへ届ける。`docker exec` で `~/.vault-token`(`0600`)へ書く。対象のコンテナ名の解決と、replica ごとの繰り返しを持つ | -| `commands/container.py` `_push_bao_token()`(足す) | `up` の [5/6] の後に呼ぶ。backend が `openbao` でなければ何もしない。失敗しても `up` を倒さない(`_apply_window_titles` と同じ扱い) | +| `env/container_token.py`(新設) | 受け取った token をコンテナへ届ける。`docker exec` で `~/.vault-token`(`0600`)へ書く。replica ごとの繰り返しを持つ。token の取得と backend の判定は持たない(呼び出し側の責務) | +| `commands/container.py` `_push_bao_token()`(足す) | `up` の [5/6] の後に呼ぶ。backend が `openbao` でなければ何もしない。`issue_token()` で token を得て `push()` へ渡す。失敗しても `up` を倒さない(`_apply_window_titles` と同じ扱い) | | `commands/container.py` `_generate_compose_for()` の `dev_environment`(変える) | backend が `openbao` のとき `BAO_ADDR=` を dev サービスの `environment` に足す(値はリテラル。機密ではない) | -| `commands/env.py` `cmd_env_token()`(足す) | `devbase env token [--print]`。既定は現在地のプロジェクトの起動中の dev コンテナへ届ける。`--print` は標準出力へ token だけを出す | -| `cli.py` の parser(変える) | `env token` のサブコマンドと `--print` | +| `commands/env.py` `cmd_env_token()`(足す) | `devbase env token [--print]`。既定は現在地のプロジェクトの起動中の dev コンテナへ届ける(`issue_token()` → `push()`)。`--print` は標準出力へ token だけを出す | +| `cli.py`(変える) | parser に `env token` のサブコマンドと `--print` を足し、`SUBCMD_MAP[('env',)]` に `token` を足す(`tests/cli/test_prefix_resolution.py` が parser と `SUBCMD_MAP` の一致を固定している)。`_NO_SECRET_INJECTION` に `('env', 'token')` を足す | | `docs/user/env-backend.md`(変える) | 「コンテナの中から `bao` を使う」の節(F4) | | `tests/containers/test_base_dockerfile_bao.py`(新設) | Dockerfile の `bao` 導入行を固定する(版・両アーキテクチャ・検証) | -| `tests/env/test_container_token.py`(新設) | `docker exec` の呼び出しの形(コマンド・stdin・umask)と、replica の繰り返し | +| `tests/env/test_container_token.py`(新設) | `docker exec` の呼び出しの形(コマンド・stdin・umask・一時ファイルからの `mv`)と、replica の繰り返し | | `tests/commands/test_container_bao.py`(新設) | `up` が `BAO_ADDR` を足す/足さない、`_push_bao_token` の要否 | | `tests/commands/test_env_token.py`(新設) | `env token` の `--print` と既定の経路、backend が `openbao` でないときの失敗 | @@ -51,13 +51,14 @@ graph LR end SRV[(OpenBao サーバ)] UP -->|environment に BAO_ADDR| ENVV - UP --> PUSH - TOKCMD --> PUSH - PUSH -->|issue_token| OB + UP -->|issue_token| OB + TOKCMD -->|issue_token| OB + UP -->|push token| PUSH + TOKCMD -->|push token| PUSH OB -->|approle login| SRV OB --> CFG OB --> BOOT - PUSH -->|docker exec: cat > ~/.vault-token| TF + PUSH -->|docker exec: 一時ファイル → mv ~/.vault-token| TF BAO --> TF BAO --> ENVV BAO -->|kv get / put / patch| SRV @@ -87,10 +88,21 @@ classDiagram +config: BackendConfig -_selected_backend() SecretBackend } - ContainerTokenPusher ..> OpenBaoBackend : issue_token() - ContainerTokenPusher ..> SecretStore : backend が openbao か + class TokenCaller { + <<呼び出し側>> + _push_bao_token() + cmd_env_token() + } + TokenCaller ..> SecretStore : backend が openbao か + TokenCaller ..> OpenBaoBackend : issue_token() + TokenCaller ..> ContainerTokenPusher : push(names, token) ``` +責務の境界は「token を得る」と「token を届ける」で分ける。`_push_bao_token` と `cmd_env_token` +(図の `TokenCaller`)が backend を判定し、`issue_token()` で token を得て `push()` へ渡す。 +`ContainerTokenPusher` は `docker` しか知らず、`OpenBaoBackend` にも `SecretStore` にも依存しない +(テストは token の文字列を渡すだけで済む)。 + `issue_token()` は `_ensure_token()` をそのまま返す。ログイン失敗の例外(`SecretAuthError` / `SecretUnreachableError`)はそのまま上へ伝える。`push()` は書けたコンテナ名の一覧を返し、 書けなかったものは警告を 1 行ずつ出す(`up` の後処理では失敗を握り、`env token` では非ゼロで @@ -112,19 +124,30 @@ classDiagram ### `devbase env token` ```text -devbase env token [--print] [-p PROJECT] +devbase env token [--print] ``` | 引数 | 意味 | | --- | --- | -| (なし) | 現在地のプロジェクト(`-p` で指定可)の起動中の dev コンテナすべての `~/.vault-token` を書き換える | -| `--print` | コンテナへ書かず、token を標準出力へ 1 行で出す。手で貼りたいとき・別の経路のコンテナのため | +| (なし) | 現在地のプロジェクトの起動中の dev コンテナすべての `~/.vault-token` を書き換える | +| `--print` | コンテナへ書かず、token を標準出力へ 1 行で出す。手で貼りたいとき・別の経路のコンテナのため。プロジェクトとコンテナは見ない | + +対象のプロジェクトは、他の `env` サブコマンド(`set -p` など)と同じく実行時のディレクトリから +決める(`_current_project_name`)。プロジェクト名を取る引数は置かない。既存の `-p` は名前を +取らない真偽フラグで(`docs/specifications/secret-backend.md`「参照の持ち主と `--user`」)、 +`env token` だけ引数を取る `-p` にすると意味が割れる。別のプロジェクトへ届けたいときは +そのディレクトリで打つ。 + +処理の順は「backend の判定 → ログイン(`issue_token()`)→ 対象の解決 → 書き込み」で、 +`--print` は 2 つ目で止まって token を出す。 | 状況 | 出力 | 終了コード | | --- | --- | --- | | backend が `openbao` でない | 「backend が openbao ではありません」 | 1 | | ログインが拒まれた(400 / 403) | 既存の `SecretAuthError` の文言 | 1 | | 到達できない | 既存の `SecretUnreachableError` の文言(**控えは使わない**。token は控えられない) | 1 | +| `--print` | token を 1 行(上の 3 つ以外の条件は見ない) | 0 | +| `projects/` の下ではない | 「プロジェクトのディレクトリで実行してください」 | 1 | | 起動中の dev コンテナが無い | 「起動中のコンテナがありません: 」 | 1 | | 一部のコンテナに書けなかった | 書けたもの・書けなかったものを 1 行ずつ | 1 | | すべて書けた | 書いたコンテナ名を 1 行ずつ(token は出さない) | 0 | @@ -134,11 +157,20 @@ devbase env token [--print] [-p PROJECT] ### `docker exec` の形 ```text -docker exec -i sh -c 'umask 077 && cat > "$HOME/.vault-token"' +docker exec -i sh -c ' + umask 077 + cat > "$HOME/.vault-token.tmp" && mv -f "$HOME/.vault-token.tmp" "$HOME/.vault-token" \ + || { rm -f "$HOME/.vault-token.tmp"; exit 1; }' (stdin: token) ``` - `-i` で stdin を渡し、引数に token を載せない(`ps` に出さない) +- 同じディレクトリの一時ファイルへ書いてから `mv -f` で置き換える(`editor/window_title.py` + `_write_command` と同じ形)。`cat >` で直接上書きすると、`env token` の再実行で既存ファイルの + mode がそのまま残り(`umask` は新規作成にしか効かない)、途中で切れると空のファイルが残る。 + 一時ファイルは毎回 `umask 077` の下で作られるため、`mv` 後の mode は前の状態によらず + `0600` になる。`window_title.py` の `cp -p`(既存の mode を写す)は要らない。ここでは + 前の mode を引き継がず、常に `0600` にしたい - 既存の `_docker_exec` の経路(`editor/window_title.py`)と同じく `DOCKER_CONTEXT` を継承する ため、リモートの daemon(PLAN52)でも同じ形で届く - `$HOME` はコンテナの利用者(`ubuntu`)のもの。`docker exec` の既定の利用者は compose の @@ -149,8 +181,9 @@ docker exec -i sh -c 'umask 077 && cat > "$HOME/.vault-token"' ```mermaid sequenceDiagram participant U as 利用者 - participant UP as cmd_up + participant UP as cmd_up / cmd_env_token participant ST as SecretStore/OpenBaoBackend + participant P as container_token.push participant D as docker participant C as dev コンテナ U->>UP: devbase up @@ -158,17 +191,20 @@ sequenceDiagram UP->>D: compose up(environment に BAO_ADDR) D->>C: 起動 UP->>UP: [5/6] ready を待つ - alt backend が openbao + alt backend が openbao(_push_bao_token) UP->>ST: issue_token()(同じインスタンス。期限内なら再ログインしない) - UP->>D: docker exec -i sh -c 'umask 077 && cat > ~/.vault-token' + UP->>P: push([…], token) + P->>D: docker exec -i sh -c '… cat > tmp && mv -f tmp ~/.vault-token' D->>C: ~/.vault-token を書く + P-->>UP: 書けたコンテナ名(失敗は警告。up は倒さない) end UP->>UP: [6/6] エディタ Note over C: 1 時間後に token が切れる U->>C: bao kv get … → 403 U->>UP: devbase env token UP->>ST: issue_token()(新しい token) - UP->>D: docker exec -i … + UP->>P: push([…], token) + P->>D: docker exec -i … U->>C: bao kv get … → 200 ``` @@ -255,7 +291,7 @@ CLI だけが要る base イメージには余計である。tar.gz は `bao` | 3. コンテナ内 `bao kv get` がホストの `env get --user` と同じ値 | 手動(リリース後テスト。PLAN53 の後の端末で) | | 4. コンテナ内 `kv patch` がホストの `env get --user` に見える | 手動(同上) | | 5. `team/global` は読めて書けない | 手動(同上。サーバのポリシーの確認) | -| 6. 1 時間後に `devbase env token` で読める | 手動(同上)。`tests/commands/test_env_token.py` で `issue_token` → `push` の順と対象コンテナを固定 | +| 6. 1 時間後に `devbase env token` で読める | 手動(同上)。`tests/commands/test_env_token.py` で `issue_token` → `push` の順と対象コンテナ、`--print` がコンテナを見ないことを固定。`tests/cli/test_prefix_resolution.py`(既存)が `SUBCMD_MAP` への登録を固定 | | 7. backend が `age` なら `BAO_ADDR` / token が無く compose が同じ | `tests/commands/test_container_bao.py`: `up` の harness で生成 compose の差分 0、`_push_bao_token` が `docker exec` を呼ばない | | 8. token がログと compose に書かれない | `tests/env/test_container_token.py`: `docker exec` の argv に token が無く stdin にある。`caplog` に token が無い | | 9. `pytest` / `ruff` / `shellcheck` | `quality-gates` | diff --git a/issues/PLAN54_bao-in-container.md b/issues/PLAN54_bao-in-container.md index acd3a65f..e876322f 100644 --- a/issues/PLAN54_bao-in-container.md +++ b/issues/PLAN54_bao-in-container.md @@ -56,15 +56,17 @@ OpenBao 側の値の変更を指し、シェルへ読み直す手段は**案内**(`docs/user/`)で示す (例: `export KEY="$(bao kv get -field=KEY …)"`)。成否の判定: 起動中のプロセスの 環境を書き換える仕組みを作らない -- 前提 7: backend が `openbao` でない端末(`age` / `plaintext`)では、`BAO_ADDR` / - `BAO_TOKEN` をコンテナへ渡さず、それ以外の振る舞いは変えない(PLAN51 の前提 3) +- 前提 7: backend が `openbao` でない端末(`age` / `plaintext`)では、~~`BAO_ADDR` / + `BAO_TOKEN`~~ → `BAO_ADDR`(環境変数)と token(`~/.vault-token`)をコンテナへ渡さず + (2026-09-14、決定 1 に揃えた)、それ以外の振る舞いは変えない(PLAN51 の前提 3) ## 対象範囲 含む: - `containers/base/Dockerfile` への `bao` 2.6.2 の導入(amd64 / arm64、checksums.txt で検証) -- backend が `openbao` のとき、`devbase up` がコンテナへ `BAO_ADDR` と有効な `BAO_TOKEN` を渡すこと +- backend が `openbao` のとき、`devbase up` がコンテナへ `BAO_ADDR`(環境変数)と、有効な token を + 書いた `~/.vault-token` を渡すこと(~~`BAO_ADDR` と有効な `BAO_TOKEN`~~ 2026-09-14、決定 1 に揃えた) - 起動中のコンテナから token を取り直す手段(方式は設計で決める) - 利用者への案内(`docs/user/env-backend.md`): パスの形、`kv put` と `kv patch` の違い、 シェルへの読み直し、token の期限と取り直し @@ -83,7 +85,7 @@ | 用語 | 意味 | | --- | --- | -| `bao` | OpenBao の CLI。`BAO_ADDR` / `BAO_TOKEN` を環境変数から読む | +| `bao` | OpenBao の CLI。接続先は環境変数 `BAO_ADDR` から、token は環境変数 `BAO_TOKEN` が無ければ `~/.vault-token` から読む | | token | AppRole ログインで得る `client_token`。TTL 1 時間、延長不可 | | 端末の資格情報 | AppRole の `role_id` / `secret_id`。ホストの `secrets/bootstrap.env.age` にある | | 控え | ホスト側 `secrets/cache/` の age 暗号化キャッシュ。読み取り専用 | @@ -107,7 +109,8 @@ `bao kv put -mount=devbase team/global X=1` 結果: 前者は成功、後者は 403 で終了コード 2(devbase 側で何も作らない。サーバの ポリシーの確認) -- [ ] 前提: コンテナ起動から 1 時間以上経ち、`BAO_TOKEN` が切れている +- [ ] 前提: コンテナ起動から 1 時間以上経ち、~~`BAO_TOKEN`~~ → `~/.vault-token` の token が + 切れている(2026-09-14、決定 1 に揃えた) 操作: 設計で決めた取り直しの手段を実行してから `bao kv get …` 結果: 再起動せずに値を読める - [ ] backend が `age` の端末で `devbase up` したとき、コンテナの `env` に `BAO_ADDR` が無く、 @@ -133,7 +136,7 @@ | --- | --- | | 公開インタフェース | コンテナ内の環境変数 `BAO_ADDR` とファイル `~/.vault-token` が増える(backend が `openbao` のときだけ)。ホストのコマンド `devbase env token` が増える | | データ | スキーマ変更なし。`secrets/backend.yml` の項目が増えるかは設計で決める | -| 既存の振る舞い | base イメージに `bao` が入る(全端末)。`openbao` の端末では `devbase up` がコンテナへ 2 変数を追加で渡す | +| 既存の振る舞い | base イメージに `bao` が入る(全端末)。`openbao` の端末では `devbase up` がコンテナへ環境変数 `BAO_ADDR` を追加で渡し、`~/.vault-token` を書く | ## 検証手段 From 8b473794bd6e285f7e6927e08f264bd83b0da3e3 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Mon, 14 Sep 2026 16:42:49 +0900 Subject: [PATCH 3/6] =?UTF-8?q?docs(PLAN54):=20round=202=20=E3=81=AE?= =?UTF-8?q?=E3=83=AC=E3=83=93=E3=83=A5=E3=83=BC=E6=8C=87=E6=91=98=E3=81=AB?= =?UTF-8?q?=E5=AF=BE=E5=BF=9C=EF=BC=88mktemp=20=E3=81=AE=E4=B8=80=E6=99=82?= =?UTF-8?q?=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB=E3=80=81env=20token=20?= =?UTF-8?q?=E3=81=AE=20docker=20context=20=E8=A7=A3=E6=B1=BA=E3=80=81Docke?= =?UTF-8?q?rfile=20=E3=81=AE=E6=9C=AC=E6=95=B0=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `docker exec` の形を固定名の一時ファイルから `mktemp` へ変える。固定名だと既存の 0644 のファイルへ書いて `umask 077` が効かず、`~/.vault-token` が 0644 になる - `env token` が `cmd_env_exec` と同じ形で docker context を解決する手順を足す。 `up` が当てた `DOCKER_CONTEXT` は別 process の `env token` に残らない。 `--context NAME` を `env exec` と同じ `_add_context_arg` で足す - `containers/*/Dockerfile` の本数(10 本、dev イメージ 9 本、snapshot は対象外)を 実態に合わせる Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01S9okWVz1S7VGUsCQWVMhb3 --- issues/PLAN54_bao-in-container-design.md | 57 ++++++++++++++++-------- 1 file changed, 39 insertions(+), 18 deletions(-) diff --git a/issues/PLAN54_bao-in-container-design.md b/issues/PLAN54_bao-in-container-design.md index 259b632d..179a1bcb 100644 --- a/issues/PLAN54_bao-in-container-design.md +++ b/issues/PLAN54_bao-in-container-design.md @@ -24,13 +24,13 @@ | `env/container_token.py`(新設) | 受け取った token をコンテナへ届ける。`docker exec` で `~/.vault-token`(`0600`)へ書く。replica ごとの繰り返しを持つ。token の取得と backend の判定は持たない(呼び出し側の責務) | | `commands/container.py` `_push_bao_token()`(足す) | `up` の [5/6] の後に呼ぶ。backend が `openbao` でなければ何もしない。`issue_token()` で token を得て `push()` へ渡す。失敗しても `up` を倒さない(`_apply_window_titles` と同じ扱い) | | `commands/container.py` `_generate_compose_for()` の `dev_environment`(変える) | backend が `openbao` のとき `BAO_ADDR=` を dev サービスの `environment` に足す(値はリテラル。機密ではない) | -| `commands/env.py` `cmd_env_token()`(足す) | `devbase env token [--print]`。既定は現在地のプロジェクトの起動中の dev コンテナへ届ける(`issue_token()` → `push()`)。`--print` は標準出力へ token だけを出す | -| `cli.py`(変える) | parser に `env token` のサブコマンドと `--print` を足し、`SUBCMD_MAP[('env',)]` に `token` を足す(`tests/cli/test_prefix_resolution.py` が parser と `SUBCMD_MAP` の一致を固定している)。`_NO_SECRET_INJECTION` に `('env', 'token')` を足す | +| `commands/env.py` `cmd_env_token()`(足す) | `devbase env token [--print] [--context NAME]`。既定は現在地のプロジェクトの起動中の dev コンテナへ届ける(`issue_token()` → 接続先の解決 → `push()`)。`--print` は標準出力へ token だけを出す | +| `cli.py`(変える) | parser に `env token` のサブコマンドと `--print` を足し、`--context` は `env exec` と同じ `_add_context_arg` で足す。`SUBCMD_MAP[('env',)]` に `token` を足す(`tests/cli/test_prefix_resolution.py` が parser と `SUBCMD_MAP` の一致を固定している)。`_NO_SECRET_INJECTION` に `('env', 'token')` を足す | | `docs/user/env-backend.md`(変える) | 「コンテナの中から `bao` を使う」の節(F4) | | `tests/containers/test_base_dockerfile_bao.py`(新設) | Dockerfile の `bao` 導入行を固定する(版・両アーキテクチャ・検証) | -| `tests/env/test_container_token.py`(新設) | `docker exec` の呼び出しの形(コマンド・stdin・umask・一時ファイルからの `mv`)と、replica の繰り返し | +| `tests/env/test_container_token.py`(新設) | `docker exec` の呼び出しの形(コマンド・stdin・`umask`・`mktemp` した一時ファイルからの `mv`)と、replica の繰り返し | | `tests/commands/test_container_bao.py`(新設) | `up` が `BAO_ADDR` を足す/足さない、`_push_bao_token` の要否 | -| `tests/commands/test_env_token.py`(新設) | `env token` の `--print` と既定の経路、backend が `openbao` でないときの失敗 | +| `tests/commands/test_env_token.py`(新設) | `env token` の `--print` と既定の経路、backend が `openbao` でないときの失敗、`project.local.yml` の `docker.context` が `docker ps` / `docker exec` の両方に効くこと | 構成要素の関係: @@ -124,13 +124,14 @@ classDiagram ### `devbase env token` ```text -devbase env token [--print] +devbase env token [--print] [--context NAME] ``` | 引数 | 意味 | | --- | --- | | (なし) | 現在地のプロジェクトの起動中の dev コンテナすべての `~/.vault-token` を書き換える | | `--print` | コンテナへ書かず、token を標準出力へ 1 行で出す。手で貼りたいとき・別の経路のコンテナのため。プロジェクトとコンテナは見ない | +| `--context NAME` | docker context を一時的に上書きする。`env exec` と同じ `_add_context_arg`(PLAN52) | 対象のプロジェクトは、他の `env` サブコマンド(`set -p` など)と同じく実行時のディレクトリから 決める(`_current_project_name`)。プロジェクト名を取る引数は置かない。既存の `-p` は名前を @@ -138,9 +139,20 @@ devbase env token [--print] `env token` だけ引数を取る `-p` にすると意味が割れる。別のプロジェクトへ届けたいときは そのディレクトリで打つ。 -処理の順は「backend の判定 → ログイン(`issue_token()`)→ 対象の解決 → 書き込み」で、 +処理の順は「backend の判定 → ログイン(`issue_token()`)→ 接続先の解決 → 対象の解決 → 書き込み」で、 `--print` は 2 つ目で止まって token を出す。 +接続先(docker context)は `env token` 自身が決める。`up` が `_resolve_docker_target` で当てた +`DOCKER_CONTEXT` はその process の環境にしか無く、後から別の process で打つ `env token` には +残らない。何もしないと、`project.local.yml` の `docker.context` だけでリモート(PLAN52)を +指す端末では既定の daemon を見に行き、コンテナが無いか、同名の別のコンテナへ token を書く。 +そこで `cmd_env_exec` と同じ形で決める。`_current_project_name` で決めたプロジェクトの直下の +`project.local.yml` を `load_project_local_config` で読み、`docker_context.choose_context` +(CLI `--context` > env `DEVBASE_DOCKER_CONTEXT` > ファイル > 未指定)で 1 つに決め、 +`docker_context.apply` で process の環境へ当てる。この後の `docker ps`(対象の解決)と +`docker exec`(書き込み)は同じ接続先へ向かう。gid・home の解決(`_resolve_docker_target`)は +要らない(compose を生成しない)。`--print` はここへ来ない。 + | 状況 | 出力 | 終了コード | | --- | --- | --- | | backend が `openbao` でない | 「backend が openbao ではありません」 | 1 | @@ -159,20 +171,27 @@ devbase env token [--print] ```text docker exec -i sh -c ' umask 077 - cat > "$HOME/.vault-token.tmp" && mv -f "$HOME/.vault-token.tmp" "$HOME/.vault-token" \ - || { rm -f "$HOME/.vault-token.tmp"; exit 1; }' + tmp=$(mktemp "$HOME/.vault-token.XXXXXX") || exit 1 + cat > "$tmp" && chmod 0600 "$tmp" && mv -f "$tmp" "$HOME/.vault-token" \ + || { rm -f "$tmp"; exit 1; }' (stdin: token) ``` - `-i` で stdin を渡し、引数に token を載せない(`ps` に出さない) - 同じディレクトリの一時ファイルへ書いてから `mv -f` で置き換える(`editor/window_title.py` `_write_command` と同じ形)。`cat >` で直接上書きすると、`env token` の再実行で既存ファイルの - mode がそのまま残り(`umask` は新規作成にしか効かない)、途中で切れると空のファイルが残る。 - 一時ファイルは毎回 `umask 077` の下で作られるため、`mv` 後の mode は前の状態によらず - `0600` になる。`window_title.py` の `cp -p`(既存の mode を写す)は要らない。ここでは - 前の mode を引き継がず、常に `0600` にしたい -- 既存の `_docker_exec` の経路(`editor/window_title.py`)と同じく `DOCKER_CONTEXT` を継承する - ため、リモートの daemon(PLAN52)でも同じ形で届く + mode がそのまま残り(`umask` は新規作成にしか効かない)、途中で切れると空のファイルが残る +- 一時ファイルは固定名にせず `mktemp` で**毎回新しく**作る。固定名(`.vault-token.tmp`)だと、 + その名前のファイルが既にあるとき `cat >` は既存の inode へ書き、`umask 077` は効かない + (0644 で先に置いておくと、終了コード 0 のまま `~/.vault-token` が 0644 になる。round 2 の + レビューで再現)。並行して打った `env token` 同士が 1 つの一時ファイルを取り合うこともない。 + `mktemp` は `O_EXCL` で `0600` に作るので、`mv` 後の mode は前の状態によらず `0600` になる。 + `chmod 0600` は `mktemp` の実装差への保険で、失敗すれば書き込みを止める。 + `window_title.py` の `cp -p`(既存の mode を写す)は要らない。ここでは前の mode を + 引き継がず、常に `0600` にしたい +- 接続先(`DOCKER_CONTEXT`)は呼び出し側が process の環境へ当てておく。`up` は + `_resolve_docker_target` が当てた同じ process の中で `_push_bao_token` を呼ぶ。`env token` は + 自身で決める(「`devbase env token`」の節)。どちらもリモートの daemon(PLAN52)へ同じ形で届く - `$HOME` はコンテナの利用者(`ubuntu`)のもの。`docker exec` の既定の利用者は compose の `user` 設定に従い、base イメージは `USER ubuntu` で終わる @@ -194,7 +213,7 @@ sequenceDiagram alt backend が openbao(_push_bao_token) UP->>ST: issue_token()(同じインスタンス。期限内なら再ログインしない) UP->>P: push([…], token) - P->>D: docker exec -i sh -c '… cat > tmp && mv -f tmp ~/.vault-token' + P->>D: docker exec -i sh -c '… tmp=$(mktemp …) && cat > $tmp && mv -f $tmp ~/.vault-token' D->>C: ~/.vault-token を書く P-->>UP: 書けたコンテナ名(失敗は警告。up は倒さない) end @@ -203,6 +222,8 @@ sequenceDiagram U->>C: bao kv get … → 403 U->>UP: devbase env token UP->>ST: issue_token()(新しい token) + UP->>UP: docker context を決めて当てる(project.local.yml / DEVBASE_DOCKER_CONTEXT / --context) + UP->>D: docker ps(起動中の dev コンテナ) UP->>P: push([…], token) P->>D: docker exec -i … U->>C: bao kv get … → 200 @@ -291,9 +312,9 @@ CLI だけが要る base イメージには余計である。tar.gz は `bao` | 3. コンテナ内 `bao kv get` がホストの `env get --user` と同じ値 | 手動(リリース後テスト。PLAN53 の後の端末で) | | 4. コンテナ内 `kv patch` がホストの `env get --user` に見える | 手動(同上) | | 5. `team/global` は読めて書けない | 手動(同上。サーバのポリシーの確認) | -| 6. 1 時間後に `devbase env token` で読める | 手動(同上)。`tests/commands/test_env_token.py` で `issue_token` → `push` の順と対象コンテナ、`--print` がコンテナを見ないことを固定。`tests/cli/test_prefix_resolution.py`(既存)が `SUBCMD_MAP` への登録を固定 | +| 6. 1 時間後に `devbase env token` で読める | 手動(同上)。`tests/commands/test_env_token.py` で `issue_token` → `push` の順と対象コンテナ、`--print` がコンテナを見ないこと、`project.local.yml` の `docker.context` をリモートにしたとき `docker ps` と `docker exec` が両方ともその context で呼ばれることを固定。`tests/cli/test_prefix_resolution.py`(既存)が `SUBCMD_MAP` への登録を固定 | | 7. backend が `age` なら `BAO_ADDR` / token が無く compose が同じ | `tests/commands/test_container_bao.py`: `up` の harness で生成 compose の差分 0、`_push_bao_token` が `docker exec` を呼ばない | -| 8. token がログと compose に書かれない | `tests/env/test_container_token.py`: `docker exec` の argv に token が無く stdin にある。`caplog` に token が無い | +| 8. token がログと compose に書かれない | `tests/env/test_container_token.py`: `docker exec` の argv に token が無く stdin にある。`caplog` に token が無い。シェルの文言に `mktemp` と `umask 077` があり、固定名の一時ファイルが無い | | 9. `pytest` / `ruff` / `shellcheck` | `quality-gates` | | 10. Dockerfile の導入行を固定するテスト | `tests/containers/test_base_dockerfile_bao.py`(版・`amd64` / `arm64` の分岐・`sha256sum -c`) | @@ -302,5 +323,5 @@ CLI だけが要る base イメージには余計である。tar.gz は `bao` | 項目 | 内容 | | --- | --- | | `bao` が `~/.vault-token` を既定で読むこと | バイナリの文字列(`~/.vault-token` / `BAO_TOKEN_PATH`)と OpenBao の CLI 文書から読んだ。実サーバで確かめるのはリリース後テスト。読まなければ `BAO_TOKEN_PATH` を `environment` で指す | -| 派生イメージが `USER` を変えていないこと | `containers/*/Dockerfile` の 8 本はすべて `ubuntu`(`${USERNAME}`)で終わる(2026-09-14 に数えた)。利用者側の `projects/*/compose.yml` が `user:` を変える場合は `$HOME` が変わり、その場合は `BAO_TOKEN_PATH` で指す運用になる | +| 派生イメージが `USER` を変えていないこと | `containers/*/Dockerfile` は 10 本。dev イメージは base と派生 8 本の 9 本で、`USER` を書く 8 本(base と派生 7 本)は `ubuntu`(`${USERNAME}`)で終わり、`general` は `USER` を書かず base の `ubuntu` を継ぐ(2026-09-14 に数えた)。`snapshot` は `FROM ubuntu:26.04` で `USER` が無く root だが、dev サービスではないので token の届け先に入らない(対象は起動中の dev コンテナだけ)。利用者側の `projects/*/compose.yml` が `user:` を変える場合は `$HOME` が変わり、その場合は `BAO_TOKEN_PATH` で指す運用になる | | PLAN55(#168)との順序 | 先に入った側に合わせて `SecretStore` の引き回しを決める(処理の流れの節) | From b11e92bbe03076b81a0c6c481acc239a458f6e36 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Mon, 14 Sep 2026 17:09:56 +0900 Subject: [PATCH 4/6] =?UTF-8?q?docs(PLAN54):=20round=203=20=E3=81=AE?= =?UTF-8?q?=E3=83=AC=E3=83=93=E3=83=A5=E3=83=BC=E6=8C=87=E6=91=98=E3=81=AB?= =?UTF-8?q?=E5=AF=BE=E5=BF=9C=EF=BC=88env=20token=20=E3=81=AE=E5=87=A6?= =?UTF-8?q?=E7=90=86=E3=81=AE=E9=A0=86=E3=80=81dev=20=E3=82=B3=E3=83=B3?= =?UTF-8?q?=E3=83=86=E3=83=8A=E3=81=AE=E7=B5=9E=E3=82=8A=E8=BE=BC=E3=81=BF?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `env token` のログイン(issue_token)を対象の解決の後へ移し、プロジェクトの外や 起動中の dev コンテナが無いときにサーバへ token を発行させない。状況表を処理の順に並べ直す - 対象の解決を `docker ps --filter label=com.docker.compose.project=` と `com.docker.compose.service` が `-` のものだけに限定し、DB・snapshot など 同じプロジェクトの他サービスへ書かないことを明記。`up` 側は resolve_container_name で組む - 構成要素表・シーケンス図・テスト設計 6 を同じ順に揃える Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01S9okWVz1S7VGUsCQWVMhb3 --- issues/PLAN54_bao-in-container-design.md | 39 ++++++++++++++++++------ 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/issues/PLAN54_bao-in-container-design.md b/issues/PLAN54_bao-in-container-design.md index 179a1bcb..dea993bf 100644 --- a/issues/PLAN54_bao-in-container-design.md +++ b/issues/PLAN54_bao-in-container-design.md @@ -24,7 +24,7 @@ | `env/container_token.py`(新設) | 受け取った token をコンテナへ届ける。`docker exec` で `~/.vault-token`(`0600`)へ書く。replica ごとの繰り返しを持つ。token の取得と backend の判定は持たない(呼び出し側の責務) | | `commands/container.py` `_push_bao_token()`(足す) | `up` の [5/6] の後に呼ぶ。backend が `openbao` でなければ何もしない。`issue_token()` で token を得て `push()` へ渡す。失敗しても `up` を倒さない(`_apply_window_titles` と同じ扱い) | | `commands/container.py` `_generate_compose_for()` の `dev_environment`(変える) | backend が `openbao` のとき `BAO_ADDR=` を dev サービスの `environment` に足す(値はリテラル。機密ではない) | -| `commands/env.py` `cmd_env_token()`(足す) | `devbase env token [--print] [--context NAME]`。既定は現在地のプロジェクトの起動中の dev コンテナへ届ける(`issue_token()` → 接続先の解決 → `push()`)。`--print` は標準出力へ token だけを出す | +| `commands/env.py` `cmd_env_token()`(足す) | `devbase env token [--print] [--context NAME]`。既定は現在地のプロジェクトの起動中の dev コンテナへ届ける(プロジェクトと接続先の解決 → `docker ps` で対象の解決 → `issue_token()` → `push()`。token はコンテナが見つかってから取る)。`--print` は標準出力へ token だけを出す | | `cli.py`(変える) | parser に `env token` のサブコマンドと `--print` を足し、`--context` は `env exec` と同じ `_add_context_arg` で足す。`SUBCMD_MAP[('env',)]` に `token` を足す(`tests/cli/test_prefix_resolution.py` が parser と `SUBCMD_MAP` の一致を固定している)。`_NO_SECRET_INJECTION` に `('env', 'token')` を足す | | `docs/user/env-backend.md`(変える) | 「コンテナの中から `bao` を使う」の節(F4) | | `tests/containers/test_base_dockerfile_bao.py`(新設) | Dockerfile の `bao` 導入行を固定する(版・両アーキテクチャ・検証) | @@ -139,8 +139,25 @@ devbase env token [--print] [--context NAME] `env token` だけ引数を取る `-p` にすると意味が割れる。別のプロジェクトへ届けたいときは そのディレクトリで打つ。 -処理の順は「backend の判定 → ログイン(`issue_token()`)→ 接続先の解決 → 対象の解決 → 書き込み」で、 -`--print` は 2 つ目で止まって token を出す。 +処理の順は「backend の判定 → プロジェクトの解決 → 接続先の解決 → 対象の解決 → ログイン +(`issue_token()`)→ 書き込み」で、`--print` は backend の判定の後すぐログインして token を出す +(プロジェクトもコンテナも見ない)。ログインを対象の解決より**後**に置くのは、届け先が無い +(`projects/` の下ではない、起動中の dev コンテナが無い)ときにサーバへ token を発行させない +ためである。発行した token は使われないまま 1 時間サーバに残る。サーバへ届かない端末では +「プロジェクトの外で打った」より先に「サーバへ到達できない」が出て、本当の誤りが読めない +(round 3 のレビュー)。 + +対象の解決は `docker ps` 1 回で行う。`--filter label=com.docker.compose.project=` で +現在地のプロジェクトに絞り、`--format '{{.Names}}\t{{.Label "com.docker.compose.service"}}'` で +名前とサービス名を取り、サービス名が `-`(`` は `get_dev_service_name()`、`` は +1 以上の整数)のものだけを残す。`up` が生成する `.docker-compose.scale.yml` は dev の各インスタンスを +サービス `{dev}-{i}`・`container_name` `${COMPOSE_PROJECT_NAME}-{dev}-{i}` で定義する +(`volume/compose.py` `_build_scaled_services`)ので、このラベルで dev 以外のサービス(DB、 +`snapshot` など同じプロジェクトのコンテナ)が届け先に入らない。プロジェクト名だけで絞ると +それらにも `docker exec` を打ち、`$HOME` の違いで失敗するか root の home に token を残す。 +`up` の `_push_bao_token` は `docker ps` を使わず、`_apply_window_titles` と同じく +`opener.resolve_container_name(dev, project, index)` を 1..scale で回して名前を組む +(起動直後で scale が分かっている)。 接続先(docker context)は `env token` 自身が決める。`up` が `_resolve_docker_target` で当てた `DOCKER_CONTEXT` はその process の環境にしか無く、後から別の process で打つ `env token` には @@ -153,14 +170,16 @@ devbase env token [--print] [--context NAME] `docker exec`(書き込み)は同じ接続先へ向かう。gid・home の解決(`_resolve_docker_target`)は 要らない(compose を生成しない)。`--print` はここへ来ない。 +状況は処理の順に並べる(上の行で止まれば下は見ない)。 + | 状況 | 出力 | 終了コード | | --- | --- | --- | | backend が `openbao` でない | 「backend が openbao ではありません」 | 1 | +| `--print` | token を 1 行(backend の判定とログインの結果以外の条件は見ない) | 0 | +| `projects/` の下ではない | 「プロジェクトのディレクトリで実行してください」(ログインしない) | 1 | +| 起動中の dev コンテナが無い | 「起動中の dev コンテナがありません: 」(ログインしない) | 1 | | ログインが拒まれた(400 / 403) | 既存の `SecretAuthError` の文言 | 1 | | 到達できない | 既存の `SecretUnreachableError` の文言(**控えは使わない**。token は控えられない) | 1 | -| `--print` | token を 1 行(上の 3 つ以外の条件は見ない) | 0 | -| `projects/` の下ではない | 「プロジェクトのディレクトリで実行してください」 | 1 | -| 起動中の dev コンテナが無い | 「起動中のコンテナがありません: 」 | 1 | | 一部のコンテナに書けなかった | 書けたもの・書けなかったものを 1 行ずつ | 1 | | すべて書けた | 書いたコンテナ名を 1 行ずつ(token は出さない) | 0 | @@ -221,9 +240,9 @@ sequenceDiagram Note over C: 1 時間後に token が切れる U->>C: bao kv get … → 403 U->>UP: devbase env token - UP->>ST: issue_token()(新しい token) - UP->>UP: docker context を決めて当てる(project.local.yml / DEVBASE_DOCKER_CONTEXT / --context) - UP->>D: docker ps(起動中の dev コンテナ) + UP->>UP: プロジェクトと docker context を決めて当てる(project.local.yml / DEVBASE_DOCKER_CONTEXT / --context) + UP->>D: docker ps --filter label=com.docker.compose.project=(service が - のものだけ残す) + UP->>ST: issue_token()(新しい token。コンテナが見つかってから) UP->>P: push([…], token) P->>D: docker exec -i … U->>C: bao kv get … → 200 @@ -312,7 +331,7 @@ CLI だけが要る base イメージには余計である。tar.gz は `bao` | 3. コンテナ内 `bao kv get` がホストの `env get --user` と同じ値 | 手動(リリース後テスト。PLAN53 の後の端末で) | | 4. コンテナ内 `kv patch` がホストの `env get --user` に見える | 手動(同上) | | 5. `team/global` は読めて書けない | 手動(同上。サーバのポリシーの確認) | -| 6. 1 時間後に `devbase env token` で読める | 手動(同上)。`tests/commands/test_env_token.py` で `issue_token` → `push` の順と対象コンテナ、`--print` がコンテナを見ないこと、`project.local.yml` の `docker.context` をリモートにしたとき `docker ps` と `docker exec` が両方ともその context で呼ばれることを固定。`tests/cli/test_prefix_resolution.py`(既存)が `SUBCMD_MAP` への登録を固定 | +| 6. 1 時間後に `devbase env token` で読める | 手動(同上)。`tests/commands/test_env_token.py` で `docker ps` → `issue_token` → `push` の順、`projects/` の外と起動中の dev コンテナが無いときに `issue_token` を呼ばないこと、`docker ps` の結果から service が `-` でないコンテナ(DB・`snapshot`)を外すこと、`--print` がコンテナを見ないこと、`project.local.yml` の `docker.context` をリモートにしたとき `docker ps` と `docker exec` が両方ともその context で呼ばれることを固定。`tests/cli/test_prefix_resolution.py`(既存)が `SUBCMD_MAP` への登録を固定 | | 7. backend が `age` なら `BAO_ADDR` / token が無く compose が同じ | `tests/commands/test_container_bao.py`: `up` の harness で生成 compose の差分 0、`_push_bao_token` が `docker exec` を呼ばない | | 8. token がログと compose に書かれない | `tests/env/test_container_token.py`: `docker exec` の argv に token が無く stdin にある。`caplog` に token が無い。シェルの文言に `mktemp` と `umask 077` があり、固定名の一時ファイルが無い | | 9. `pytest` / `ruff` / `shellcheck` | `quality-gates` | From 9941a2070ae372e0b43ac6c84643e970e6987c81 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Mon, 14 Sep 2026 17:21:54 +0900 Subject: [PATCH 5/6] =?UTF-8?q?docs(PLAN54):=20=E8=A8=AD=E8=A8=88=E3=81=AE?= =?UTF-8?q?=E6=A7=8B=E6=88=90=E8=A6=81=E7=B4=A0=E8=A1=A8=E3=81=AB=20docs/s?= =?UTF-8?q?pecifications/secret-backend.md=20=E3=81=AE=E8=BF=BD=E8=A8=98?= =?UTF-8?q?=E3=82=92=E6=88=90=E6=9E=9C=E7=89=A9=E3=81=A8=E3=81=97=E3=81=A6?= =?UTF-8?q?=E8=BC=89=E3=81=9B=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 要求仕様の対象範囲が含む「確定仕様の追記(plan-to-spec)」が設計の 構成要素表に無く、実装で抜ける形だった。行を足し、spec のどの節へ何を 足すか(構成要素・OpenBao との契約・env token の節・セキュリティ・運用・ テスト観点)を書いた。テスト設計にも受け入れ条件を持たない成果物として plan-to-spec 時の照合を記した。 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01S9okWVz1S7VGUsCQWVMhb3 --- issues/PLAN54_bao-in-container-design.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/issues/PLAN54_bao-in-container-design.md b/issues/PLAN54_bao-in-container-design.md index dea993bf..05bf330b 100644 --- a/issues/PLAN54_bao-in-container-design.md +++ b/issues/PLAN54_bao-in-container-design.md @@ -27,6 +27,7 @@ | `commands/env.py` `cmd_env_token()`(足す) | `devbase env token [--print] [--context NAME]`。既定は現在地のプロジェクトの起動中の dev コンテナへ届ける(プロジェクトと接続先の解決 → `docker ps` で対象の解決 → `issue_token()` → `push()`。token はコンテナが見つかってから取る)。`--print` は標準出力へ token だけを出す | | `cli.py`(変える) | parser に `env token` のサブコマンドと `--print` を足し、`--context` は `env exec` と同じ `_add_context_arg` で足す。`SUBCMD_MAP[('env',)]` に `token` を足す(`tests/cli/test_prefix_resolution.py` が parser と `SUBCMD_MAP` の一致を固定している)。`_NO_SECRET_INJECTION` に `('env', 'token')` を足す | | `docs/user/env-backend.md`(変える) | 「コンテナの中から `bao` を使う」の節(F4) | +| `docs/specifications/secret-backend.md`(変える。実装 PR のマージ後に `plan-to-spec` で。要求仕様の対象範囲の最後の項目) | 確定仕様の追記先。節ごとに足すもの: 「構成要素」の表に `env/container_token.py` の行、図に上の 2 本の辺(`bao` → サーバ、ホスト → コンテナの `~/.vault-token`)。「OpenBao との契約」の「token は実行のたびに取り直し、ディスクへ保存しない」を「ホストでは保存せず、コンテナへは `docker exec` の stdin で渡して `~/.vault-token`(`0600`)に置く」へ改め、`BAO_ADDR`(compose の `environment`、リテラル)と `~/.vault-token`(`mktemp` → `mv -f`、token は argv に出さない)の入出力の契約を箇条書きで足す(#168(PLAN55)も同じ節へ足す予定なので、節を新設せず箇条書きを増やす)。「`devbase env backend`」の次に「`devbase env token`」の節(引数・処理の順・終了コード。この文書の「入出力の契約」を移す)。「セキュリティ」に「コンテナに置く資格情報は 1 時間の token だけで `secret_id` はホストから出ない。token を環境変数にしない理由」(決定 1)。「運用」に token の期限と `devbase env token` での取り直し。「テスト観点」にこの表の新設テスト 4 本 | | `tests/containers/test_base_dockerfile_bao.py`(新設) | Dockerfile の `bao` 導入行を固定する(版・両アーキテクチャ・検証) | | `tests/env/test_container_token.py`(新設) | `docker exec` の呼び出しの形(コマンド・stdin・`umask`・`mktemp` した一時ファイルからの `mv`)と、replica の繰り返し | | `tests/commands/test_container_bao.py`(新設) | `up` が `BAO_ADDR` を足す/足さない、`_push_bao_token` の要否 | @@ -337,6 +338,8 @@ CLI だけが要る base イメージには余計である。tar.gz は `bao` | 9. `pytest` / `ruff` / `shellcheck` | `quality-gates` | | 10. Dockerfile の導入行を固定するテスト | `tests/containers/test_base_dockerfile_bao.py`(版・`amd64` / `arm64` の分岐・`sha256sum -c`) | +成果物のうち `docs/specifications/secret-backend.md` の追記は受け入れ条件を持たず、テストも無い。`plan-to-spec` のとき、構成要素の表の同じ行に書いた節の一覧と追記後の spec の見出しを突き合わせる。 + ## 未確認のまま残ること | 項目 | 内容 | From f3940b07c4d71318141826684674eb1602409aeb Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Mon, 14 Sep 2026 17:38:22 +0900 Subject: [PATCH 6/6] =?UTF-8?q?docs(PLAN54):=20round=205=20=E3=81=AE?= =?UTF-8?q?=E3=83=AC=E3=83=93=E3=83=A5=E3=83=BC=E6=8C=87=E6=91=98=E3=81=AB?= =?UTF-8?q?=E5=AF=BE=E5=BF=9C=EF=BC=88SecretStore=20=E3=81=AE=E4=BF=9D?= =?UTF-8?q?=E6=8C=81=E3=81=AF=E5=A4=89=E6=9B=B4=E3=81=A8=E3=81=97=E3=81=A6?= =?UTF-8?q?=E6=9B=B8=E3=81=8F=E3=80=81env=20token=20=E3=81=AF=E7=9B=B4?= =?UTF-8?q?=E4=B8=8B=E3=81=AE=20env=20=E3=81=8B=E3=82=89=20dev=20=E3=82=B5?= =?UTF-8?q?=E3=83=BC=E3=83=93=E3=82=B9=E5=90=8D=E3=82=92=E5=8F=96=E3=82=8B?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `_run_deploy_pipeline` の `SecretStore` は現行コードが保持していないことを明記し、 注入と `_push_bao_token` へ同じ store を渡す「変更」として書き直す - `devbase env token` が下位ディレクトリから打たれても `DEV_SERVICE_NAME` を拾えるよう、 対象の解決の前に解決済みプロジェクト直下の `env` から dev サービス名を取る手順を足し、 処理の順・構成要素表・シーケンス図・テスト設計 6 を揃える Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01S9okWVz1S7VGUsCQWVMhb3 --- issues/PLAN54_bao-in-container-design.md | 31 ++++++++++++++++++------ 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/issues/PLAN54_bao-in-container-design.md b/issues/PLAN54_bao-in-container-design.md index 05bf330b..8b643191 100644 --- a/issues/PLAN54_bao-in-container-design.md +++ b/issues/PLAN54_bao-in-container-design.md @@ -24,7 +24,7 @@ | `env/container_token.py`(新設) | 受け取った token をコンテナへ届ける。`docker exec` で `~/.vault-token`(`0600`)へ書く。replica ごとの繰り返しを持つ。token の取得と backend の判定は持たない(呼び出し側の責務) | | `commands/container.py` `_push_bao_token()`(足す) | `up` の [5/6] の後に呼ぶ。backend が `openbao` でなければ何もしない。`issue_token()` で token を得て `push()` へ渡す。失敗しても `up` を倒さない(`_apply_window_titles` と同じ扱い) | | `commands/container.py` `_generate_compose_for()` の `dev_environment`(変える) | backend が `openbao` のとき `BAO_ADDR=` を dev サービスの `environment` に足す(値はリテラル。機密ではない) | -| `commands/env.py` `cmd_env_token()`(足す) | `devbase env token [--print] [--context NAME]`。既定は現在地のプロジェクトの起動中の dev コンテナへ届ける(プロジェクトと接続先の解決 → `docker ps` で対象の解決 → `issue_token()` → `push()`。token はコンテナが見つかってから取る)。`--print` は標準出力へ token だけを出す | +| `commands/env.py` `cmd_env_token()`(足す) | `devbase env token [--print] [--context NAME]`。既定は現在地のプロジェクトの起動中の dev コンテナへ届ける(プロジェクトの解決 → プロジェクト直下の `env` から dev サービス名の解決 → 接続先の解決 → `docker ps` で対象の解決 → `issue_token()` → `push()`。token はコンテナが見つかってから取る)。`--print` は標準出力へ token だけを出す | | `cli.py`(変える) | parser に `env token` のサブコマンドと `--print` を足し、`--context` は `env exec` と同じ `_add_context_arg` で足す。`SUBCMD_MAP[('env',)]` に `token` を足す(`tests/cli/test_prefix_resolution.py` が parser と `SUBCMD_MAP` の一致を固定している)。`_NO_SECRET_INJECTION` に `('env', 'token')` を足す | | `docs/user/env-backend.md`(変える) | 「コンテナの中から `bao` を使う」の節(F4) | | `docs/specifications/secret-backend.md`(変える。実装 PR のマージ後に `plan-to-spec` で。要求仕様の対象範囲の最後の項目) | 確定仕様の追記先。節ごとに足すもの: 「構成要素」の表に `env/container_token.py` の行、図に上の 2 本の辺(`bao` → サーバ、ホスト → コンテナの `~/.vault-token`)。「OpenBao との契約」の「token は実行のたびに取り直し、ディスクへ保存しない」を「ホストでは保存せず、コンテナへは `docker exec` の stdin で渡して `~/.vault-token`(`0600`)に置く」へ改め、`BAO_ADDR`(compose の `environment`、リテラル)と `~/.vault-token`(`mktemp` → `mv -f`、token は argv に出さない)の入出力の契約を箇条書きで足す(#168(PLAN55)も同じ節へ足す予定なので、節を新設せず箇条書きを増やす)。「`devbase env backend`」の次に「`devbase env token`」の節(引数・処理の順・終了コード。この文書の「入出力の契約」を移す)。「セキュリティ」に「コンテナに置く資格情報は 1 時間の token だけで `secret_id` はホストから出ない。token を環境変数にしない理由」(決定 1)。「運用」に token の期限と `devbase env token` での取り直し。「テスト観点」にこの表の新設テスト 4 本 | @@ -140,17 +140,27 @@ devbase env token [--print] [--context NAME] `env token` だけ引数を取る `-p` にすると意味が割れる。別のプロジェクトへ届けたいときは そのディレクトリで打つ。 -処理の順は「backend の判定 → プロジェクトの解決 → 接続先の解決 → 対象の解決 → ログイン -(`issue_token()`)→ 書き込み」で、`--print` は backend の判定の後すぐログインして token を出す +処理の順は「backend の判定 → プロジェクトの解決 → dev サービス名の解決 → 接続先の解決 → +対象の解決 → ログイン(`issue_token()`)→ 書き込み」で、`--print` は backend の判定の後すぐログインして token を出す (プロジェクトもコンテナも見ない)。ログインを対象の解決より**後**に置くのは、届け先が無い (`projects/` の下ではない、起動中の dev コンテナが無い)ときにサーバへ token を発行させない ためである。発行した token は使われないまま 1 時間サーバに残る。サーバへ届かない端末では 「プロジェクトの外で打った」より先に「サーバへ到達できない」が出て、本当の誤りが読めない (round 3 のレビュー)。 +dev サービス名は、対象の解決より**前**に、解決済みプロジェクトの直下の非機密設定 +`projects//env` から取る。起動ラッパー `bin/devbase` が `source ./env` するのは +実行時のディレクトリの `env` だけなので、`projects//src` などの下位ディレクトリから +打つと `DEV_SERVICE_NAME` が環境に載らず、`get_dev_service_name()` は既定の `dev` を返す。 +`env` に `DEV_SERVICE_NAME=workspace` を置いて起動した `workspace-1` が、`-` の絞り込みで +対象から外れる(round 5 のレビュー)。そこで `_current_project_name` で決めたプロジェクトの +`env` を `_load_project_env`(`commands/container.py`。ラッパーと同じ `KEY=VALUE` の解釈)で +`os.environ` へ載せてから `get_dev_service_name()` を呼ぶ。プロジェクト直下で打ったときは +ラッパーが載せた値と同じ値を載せ直すだけで、結果は変わらない。 + 対象の解決は `docker ps` 1 回で行う。`--filter label=com.docker.compose.project=` で 現在地のプロジェクトに絞り、`--format '{{.Names}}\t{{.Label "com.docker.compose.service"}}'` で -名前とサービス名を取り、サービス名が `-`(`` は `get_dev_service_name()`、`` は +名前とサービス名を取り、サービス名が `-`(`` は上で解決した dev サービス名、`` は 1 以上の整数)のものだけを残す。`up` が生成する `.docker-compose.scale.yml` は dev の各インスタンスを サービス `{dev}-{i}`・`container_name` `${COMPOSE_PROJECT_NAME}-{dev}-{i}` で定義する (`volume/compose.py` `_build_scaled_services`)ので、このラベルで dev 以外のサービス(DB、 @@ -241,7 +251,8 @@ sequenceDiagram Note over C: 1 時間後に token が切れる U->>C: bao kv get … → 403 U->>UP: devbase env token - UP->>UP: プロジェクトと docker context を決めて当てる(project.local.yml / DEVBASE_DOCKER_CONTEXT / --context) + UP->>UP: プロジェクトを決め、その直下の env から dev サービス名を取る(下位ディレクトリからでも同じ) + UP->>UP: docker context を決めて当てる(project.local.yml / DEVBASE_DOCKER_CONTEXT / --context) UP->>D: docker ps --filter label=com.docker.compose.project=(service が - のものだけ残す) UP->>ST: issue_token()(新しい token。コンテナが見つかってから) UP->>P: push([…], token) @@ -250,8 +261,12 @@ sequenceDiagram ``` `up` の途中で `issue_token()` が返す token は、`_inject_secrets(required=True)` と同じ -インスタンスのものである。`_run_deploy_pipeline` が `SecretEnv` を作った `SecretStore` を -保持して渡す。`up` の往復は増えない。 +インスタンスのものにする。現行の `_inject_secrets` は `runtime.inject(root, project)` を +`store` 引数なしで呼び、戻り値の `SecretEnv` をその場で `_generate_compose_for` へ渡すだけで、 +`SecretStore` は保持していない(`commands/container.py`)。この実装で、`_run_deploy_pipeline` +が `SecretStore` を 1 つ作って `inject(..., store=)` へ渡し、同じものを `_push_bao_token` へも +渡すよう変える(下の表「PLAN55 の前」)。既存コードに再利用できる `SecretStore` があるわけでは +ない。こうして `up` の往復を増やさない(ログインは注入と token で 1 回)。 **#168(PLAN55)が `SecretStore` の引き回しを変える場合は、この呼び出しもそちらの経路に乗せる。** @@ -332,7 +347,7 @@ CLI だけが要る base イメージには余計である。tar.gz は `bao` | 3. コンテナ内 `bao kv get` がホストの `env get --user` と同じ値 | 手動(リリース後テスト。PLAN53 の後の端末で) | | 4. コンテナ内 `kv patch` がホストの `env get --user` に見える | 手動(同上) | | 5. `team/global` は読めて書けない | 手動(同上。サーバのポリシーの確認) | -| 6. 1 時間後に `devbase env token` で読める | 手動(同上)。`tests/commands/test_env_token.py` で `docker ps` → `issue_token` → `push` の順、`projects/` の外と起動中の dev コンテナが無いときに `issue_token` を呼ばないこと、`docker ps` の結果から service が `-` でないコンテナ(DB・`snapshot`)を外すこと、`--print` がコンテナを見ないこと、`project.local.yml` の `docker.context` をリモートにしたとき `docker ps` と `docker exec` が両方ともその context で呼ばれることを固定。`tests/cli/test_prefix_resolution.py`(既存)が `SUBCMD_MAP` への登録を固定 | +| 6. 1 時間後に `devbase env token` で読める | 手動(同上)。`tests/commands/test_env_token.py` で `docker ps` → `issue_token` → `push` の順、`projects/` の外と起動中の dev コンテナが無いときに `issue_token` を呼ばないこと、`docker ps` の結果から service が `-` でないコンテナ(DB・`snapshot`)を外すこと、`env` に `DEV_SERVICE_NAME=workspace` を持つプロジェクトの `projects//src`(下位ディレクトリ)を CWD にし `DEV_SERVICE_NAME` を環境に載せない状態で、`workspace-1` が対象に残り `docker exec` が呼ばれること(プロジェクト直下の `env` から dev サービス名を取る条件の検証)、`--print` がコンテナを見ないこと、`project.local.yml` の `docker.context` をリモートにしたとき `docker ps` と `docker exec` が両方ともその context で呼ばれることを固定。`tests/cli/test_prefix_resolution.py`(既存)が `SUBCMD_MAP` への登録を固定 | | 7. backend が `age` なら `BAO_ADDR` / token が無く compose が同じ | `tests/commands/test_container_bao.py`: `up` の harness で生成 compose の差分 0、`_push_bao_token` が `docker exec` を呼ばない | | 8. token がログと compose に書かれない | `tests/env/test_container_token.py`: `docker exec` の argv に token が無く stdin にある。`caplog` に token が無い。シェルの文言に `mktemp` と `umask 077` があり、固定名の一時ファイルが無い | | 9. `pytest` / `ruff` / `shellcheck` | `quality-gates` |