diff --git a/docs/specifications/secret-backend.md b/docs/specifications/secret-backend.md index 93ec32ae..b925ca8f 100644 --- a/docs/specifications/secret-backend.md +++ b/docs/specifications/secret-backend.md @@ -14,6 +14,14 @@ KV v2 シークレットエンジンに対応し、REST を標準ライブラリ あり、`devbase env list` / `get` / `set` / `delete` / `edit` の `--user` で個人単位の置き場を 相手にする。ファイル backend は個人単位の置き場を持たない。 +OpenBao の置き場の並び(レイアウト)は `backend.yml` の版で選ぶ。`version: 1` はパスに +グループを含まず(`team/global` など)、`version: 2` はチーム単位と個人単位の置き場を +アカウントグループ(`DEVBASE_ACCOUNT_GROUP`、ボリューム `devbase_home_` と同じ単位) +ごとに分ける(`team//global` など)。`version: 2` では、プロジェクトのコンテナへ届く機密は +そのプロジェクトのグループの置き場のものだけで、1 回の操作が要求するパスも対象のグループの +ものだけになる。パスの先頭側でグループが分かれるため、サーバはグループ単位に読み書きを +許せる。ファイル backend はグループで分けない。 + backend が `openbao` の端末では、dev コンテナの中の OpenBao CLI(`bao`、base イメージに同梱)が 接続先 `BAO_ADDR` と `~/.vault-token` を受け取り、再起動せずに自分の機密を読み書きできる。 コンテナに置く資格情報は 1 時間で切れる token だけで、切れたらホストの `devbase env token` で @@ -39,7 +47,12 @@ Infisical で個人単位の機密を守るには利用者ごとに project を | 用語 | 意味 | | --- | --- | -| 参照(`SecretRef`) | 機密の宛先。適用範囲(`global` / `project`)と持ち主(`team` / `user`)の組み合わせで 4 種 | +| 参照(`SecretRef`) | 機密の宛先。適用範囲(`global` / `project`)と持ち主(`team` / `user`)の組み合わせで 4 種。`version: 2` ではグループも持つ | +| アカウントグループ(グループ) | `DEVBASE_ACCOUNT_GROUP` の値。未設定なら `default`。ボリューム `devbase_home_` の単位でもある | +| レイアウト(`layout`) | 置き場のパスの並び。`flat`(`version: 1`、グループを含まない)と `group`(`version: 2`、グループを含む) | +| `group_aliases` | グループ名から置き場のグループ名への対応。`backend.yml` の `openbao` 節に置き、ボリューム名を変えずに置き場の上だけ読み替える | +| 置き場のグループ名 | パスに入れる名前。グループ名を `group_aliases` で読み替えた後の名前(対応が無ければグループ名のまま)。以下 `` と書く | +| 対象のグループ | 1 回の操作が読み書きするグループ。既定では実行時のディレクトリのプロジェクトのグループ(プロジェクトの外なら `$DEVBASE_ROOT/env` のグループ) | | チーム単位の機密 | チームの全員が同じ値を使う機密(サービスアカウントの鍵、連携先の API キーなど) | | 個人単位の機密 | 利用者ごとに値が違う機密(各自のクラウドアクセスキー、個人アクセストークンなど) | | backend | 参照に対して機密を読み書きする実装。`plaintext` / `age` / `openbao`。`auto` は存在による判定 | @@ -55,27 +68,33 @@ Infisical で個人単位の機密を守るには利用者ごとに project を | 要素 | 置き場所 | 責務 | | --- | --- | --- | -| backend の設定 | `lib/devbase/env/backend_config.py` | `secrets/backend.yml` の読み書きと検証、参照ごとのパスの組み立て | +| backend の設定 | `lib/devbase/env/backend_config.py` | `secrets/backend.yml` の読み書きと検証(版とレイアウト、`group_aliases`)、置き場のグループ名への読み替え(`storage_group`)、参照ごとのパス・キャッシュの位置・`index.json` のキーの組み立て(`path_of` / `cache_relpath` / `cache_key`) | +| グループの決定 | `lib/devbase/env/groups.py` | 非機密の `env` ファイルだけから `DEVBASE_ACCOUNT_GROUP` を決める(`declare` / `declared_group`)。決めたファイルの表示(`describe_source`) | | 登録簿 | `lib/devbase/env/backends.py` | backend 名から実装を作る。未知の名前は一覧を添えて拒む | -| ストアの窓口 | `lib/devbase/env/secret_store.py` | `SecretRef`(持ち主の軸)、`PlaintextBackend` / `AgeBackend`、設定を見て backend を選ぶ `SecretStore`(`fetch` は現物を読み、控えへ落ちない) | +| ストアの窓口 | `lib/devbase/env/secret_store.py` | `SecretRef`(持ち主の軸とグループ)、`PlaintextBackend` / `AgeBackend`、設定を見て backend を選ぶ `SecretStore`(`fetch` は現物を読み、控えへ落ちない。参照に持たせるグループを `ref_group`、同じ置き場かを `same_storage_group` で返す) | | 参照のビュー | `lib/devbase/env/secret_view.py` | `SecretEnvFile`。`fresh=True` なら読み出しに `fetch` を使う(`set` / `delete` / `edit` の入口) | | OpenBao adapter | `lib/devbase/env/openbao.py` | AppRole 認証、参照ごとの取得、版を指定した丸ごとの書き込み、失敗の種類の判定 | | ブートストラップ | `lib/devbase/env/bootstrap.py` | 接続資格情報を登録簿を経由せず age で直接読み書きする | | キャッシュ | `lib/devbase/env/cache.py` | 参照ごとの控えの書き込み・読み出し・破棄・全消去 | -| 機密の合成 | `lib/devbase/env/runtime.py` | 4 層の機密を重ねてコンテナへ渡す。`SecretStore` をライフサイクル操作 1 回の間持ち回る(`store_for` / `release_store`) | +| 機密の合成 | `lib/devbase/env/runtime.py` | 4 層の機密を対象のプロジェクトのグループで重ねてコンテナへ渡す。`SecretStore` をライフサイクル操作 1 回の間持ち回る(`store_for` / `release_store`) | +| dispatch 前の注入 | `lib/devbase/cli.py` | `_load_secret_env`。注入を行わないコマンドと、`version: 2` で注入に使うプロジェクトを決める。`--group` / `--layout` / `--group-alias` / `--exclude-project` の引数 | +| 同期済みハッシュの控え | `lib/devbase/env/sources.py` | `SourcesManager` と `sources_path`。`version: 2` では置き場のグループごとに控えを分ける | | コンテナへの token の配送 | `lib/devbase/env/container_token.py` | 受け取った token を `docker exec` の stdin で各コンテナの `~/.vault-token` へ書く。token の取得と届け先の解決は持たない | -| `up` / `scale` の後処理 | `lib/devbase/commands/container.py` | backend が `openbao` のとき dev サービスへ `BAO_ADDR` を足し、起動後に token を書く | +| `up` / `scale` の前処理と後処理 | `lib/devbase/commands/container.py` | `version: 2` でボリュームと機密のグループの食い違いを起動前に検査する(`_check_group_consistency`)。`_ensure_env_files` の子プロセスの `env init` へグループを渡す。backend が `openbao` のとき dev サービスへ `BAO_ADDR` を足し、起動後に token を書く | | base イメージ | `containers/base/Dockerfile` | OpenBao CLI `bao` を `checksums.txt` で検証して `/usr/local/bin` へ置く | | `env backend` コマンド | `lib/devbase/commands/env_backend.py` | `status` / `use` / `test` / `migrate` | -| `env` コマンド | `lib/devbase/commands/env.py` | `--user` の受け取り、`edit` の分岐、一覧の保存形式表示、`env token` | -| `rekey` / `doctor` | `lib/devbase/commands/env_ops.py` | 手元の age 暗号文すべての再暗号化、backend 設定と権限の点検 | +| `env` コマンド | `lib/devbase/commands/env.py` | `--user` と `--group` の受け取り、`-p` とプロジェクトのグループの照合、`edit` の分岐、一覧の保存形式表示、`env token` | +| `rekey` / `doctor` | `lib/devbase/commands/env_ops.py` | 手元の age 暗号文すべての再暗号化、backend 設定と権限と Git の除外の点検 | | `encrypt` / `decrypt` | `lib/devbase/commands/env_migrate.py` | age ストアと平文の間の移動(backend の向きと突き合わせる) | -| `import` | `lib/devbase/env/io_import.py` | サーバ backend の参照への取り込みと age 暗号化した退避。計画の元にした値(`Plan.before`)を退避と巻き戻しに使う | +| `export` | `lib/devbase/env/bundle.py` | 機密をバンドルへ集める。`version: 2` では対象のグループと同じ置き場のプロジェクトだけを集める | +| `import` | `lib/devbase/env/io_import.py` | サーバ backend の参照への取り込みと age 暗号化した退避。計画の元にした値(`Plan.before`)を退避と巻き戻しに使う。`version: 2` では別グループのプロジェクトを含むバンドルを拒む | ```mermaid flowchart LR CLI[devbase env / up] --> ST[SecretStore] ST --> CFG[backend_config] + ST -->|version 2 の ref_group| GR[groups] + GR --> ENVF[(projects/name/env と env)] ST --> REG[backends 登録簿] REG --> PT[PlaintextBackend] REG --> AGE[AgeBackend] @@ -155,16 +174,222 @@ flowchart LR プロジェクトのチーム機密に勝ち、プロジェクト専用のサービスアカウントの鍵が各自の共通設定で 上書きされるため採らない。コンテナへ列挙する変数名は 4 層のキーをこの順で並べ、重複は先に 現れた位置で 1 件に畳む。個人単位の参照を持たない backend では 2 と 5 が空になり、結果は -従来と同じである。 +従来と同じである。`version: 2` では 4 つの機密の層はいずれも、起動するプロジェクトの +グループ(`SecretStore.ref_group(project)`)の参照である。 + +### アカウントグループごとの置き場(`version: 2`) + +`backend.yml` の版はレイアウトと 1 対 1 で、`version: 1` は `flat`、`version: 2` は `group` である。 +`version: 2` では `openbao.layout: group` を必須にする(読み手が版の番号から並びを思い出さずに +済むため)。パスの対応は「OpenBao との契約」、キャッシュと控えの位置は「データ・設定」にある。 + +参照がグループを持つのは、backend が `openbao` かつ `layout: group` のときだけである。 +`version: 2` の設定のまま backend を `age` などにしても(`env backend use age` と +`migrate --to age` は `openbao` 節と版を引き継ぐ)、ファイル backend はグループで分けない。 + +**版を上げる理由。** 古い devbase は `openbao` 節の未知のキーを黙って無視する。キーを足すだけ +では、配布が行き渡っていない端末がグループ別の置き場の設定を読んで `team/global` を読み続け、 +別グループの機密をコンテナへ渡す。版を上げれば、`version` が 1 以外なら拒む規則でその端末が +止まる。パスの設定に `{group}` の差し込みを許す形は、個人単位のパスをコードが組んでいて +差し込みの位置を表すキーが増えることと、古い devbase が `{group}` を文字どおりのパスとして +読むことから採らない。 + +#### グループの決まり方 + +プロジェクトのグループは、機密を読む前に非機密の `env` ファイルだけから決まる +(`groups.declare` / `declared_group(root, project)`)。 + +1. `projects//env` の `DEVBASE_ACCOUNT_GROUP`(`project` があるとき) +2. `$DEVBASE_ROOT/env` の `DEVBASE_ACCOUNT_GROUP` +3. どちらにも無ければ `default` + +- 1 つのファイルの中は行の順に読み、`export DEVBASE_ACCOUNT_GROUP=...` の行も同じキーとして + 扱う。複数あれば最後の行が勝つ(起動ラッパーの `source` と同じ結果) +- 空の値も宣言として扱い、`default` になる(`source` では空の宣言が共通の宣言を打ち消す) +- 名前はボリューム名と同じ `volume.manager.resolve_account_group` で検証する(Docker の + ボリューム名に使える文字だけ。予約語 `ubuntu` と数字だけの名前は不可)。通らなければ + ファイルの位置を添えて拒む +- 決めたファイルの表示(`describe_source`)は `$DEVBASE_ROOT` からの相対パスで、直下の `env` は + `$DEVBASE_ROOT/env` と出す。宣言が無ければ `projects//env にも $DEVBASE_ROOT/env にも + 宣言なし`(プロジェクトの外では `$DEVBASE_ROOT/env に宣言なし`) + +**グループをファイルだけから決める理由。** 起動ラッパーは実行時のディレクトリの `env` だけを +読むため、プロジェクトの下位ディレクトリから打つとプロジェクトの `env` がプロセスに載らない。 +ファイルを直接読めば、下位ディレクトリからでも同じグループになる。機密の置き場の値を使うと、 +置き場を決める値をその置き場から読む循環になる(`declared_group` はストアを受け取らない)。 + +#### 参照のグループ + +`SecretRef` は `group`(読み替える前のグループ名、既定 `None`)を持ち、`for_global` / +`for_project` が `group=` を受けて同じ規則で検証する。グループは参照の等価性に入り、1 つの +`SecretStore` の中でグループの違う参照の控え(取得した内容と版)を取り違えない。グループを +`SecretStore` のインスタンスに持たせないのは、ストアがプロジェクトの切替をまたいで持ち回られる +ためである。`label()` はグループがあれば `(グループ <名前>)` を後ろに付ける。 + +`SecretStore.ref_group(project)` は、backend が `openbao` かつ `layout: group` のときだけ +`declared_group(root, project)` を返し、それ以外は `None` を返す。`version: 1` とファイル +backend では参照のグループが常に空で、参照の値・等価性・キャッシュの位置・往復の回数は +グループを持たない参照と同じである。グループを常に参照へ入れて `version: 1` のパスの組み立て +で無視する形は、控えの鍵にグループが入り、グループの違うプロジェクトへ切り替えたときに同じ +パスを 2 度取りに行くため採らない。 + +`OpenBaoBackend.path_of` は、`layout: group` でグループの無い参照と、`layout: flat` でグループの +付いた参照を `SecretStoreError` で拒む。取得・保存・削除とキャッシュの位置はすべてここを通る +ため、呼び出しの誤りがサーバへの要求や別グループのパスへ落ちない。 + +#### 読み替え(`group_aliases`) + +置き場のグループ名は `OpenBaoSettings.storage_group(group)` が決める。グループ名を検証し、 +`group_aliases` に対応があれば読み替え、読み替えた後の名前が `global` / `projects` なら拒む +(`team//…` が `version: 1` の `team/global` / `team/projects/` と重なるため)。 +`global` を読み替え元にする対応は受け付ける。2 つのグループが同じ置き場かは読み替えた後の +名前で比べる(`SecretStore.same_storage_group`)。文言には読み替えの前と後を `default → nyle` +の形で出す(`display_group`)。 + +**`default` の読み替えを置き場の上だけで行う理由。** `DEVBASE_ACCOUNT_GROUP` の既定値を変えると +ボリューム名 `devbase_home_default` が変わり、既存の認証と会話ログのボリュームを移すことになる。 +公開リポジトリのコードに社名を既定値として持ち込むことにもなる。読み替えを端末の設定に置けば、 +ボリュームに触らず、社名はその端末の設定にだけ入る。 + +**全グループ共通の置き場とファイル backend の分割を持たない理由。** 全グループで同じ値を使う +機密は、グループごとの置き場へ同じ値を置く。共通の置き場を持つと、そこへ企業固有の機密が +再び混ざり、1 回の操作が対象のグループ以外のパスへ要求を出す。ファイル backend の機密は +1 台の端末の中に閉じており、サーバのポリシーでグループ単位に読み書きを許すという動機が +当たらない。 + +#### `env` コマンドの `--group` + +`env list` / `get` / `set` / `delete` / `edit` / `init` は `--group NAME` を受ける。省略時は対象の +グループを使う。`sync` / `project` / `export` / `import` は `--group` を受けず、実行時の +ディレクトリで決まる。`set` / `delete` / `edit` の `-p` は宛先を、`list` の `-p` はプロジェクトの +節だけを出すことを指し、`get` は `-p` を取らずにプロジェクトの参照を実行時のディレクトリから +含める。 + +| 状況 | 結果 | +| --- | --- | +| `--group` なし | 対象のグループの参照 | +| `--group NAME`、`-p` なし | 共通の参照(個人共通を含む)を `NAME` のグループで読み書きする | +| `--group NAME` と `-p`、プロジェクトのグループと同じ置き場 | そのプロジェクトの参照を読み書きする | +| `--group NAME` と `-p`、プロジェクトのグループと違う置き場 | 読み替えの前後のグループ名と決めたファイルを述べて 1。サーバへ要求しない | +| `-p` なしの `list` / `get` で `--group NAME`、プロジェクトのグループと違う置き場 | 共通の参照だけを出す・探す。プロジェクトの参照を含めなかった旨を標準エラーへ 1 行出す | +| 使えない名前(`resolve_account_group` の規則、読み替えた後が `global` / `projects`) | 理由を述べて 2。読み書きしない | +| `version: 1` またはファイル backend で `--group` | グループ別の置き場を選んだ設定でだけ使える旨を述べて 2 | + +`-p` でプロジェクトのグループと違う置き場を拒むのは、`team/kkg/projects/web` に書けても `web` の +グループが `with` なら `up` はそこを読まず、書けたように見えて使われない機密が残るためである。 +文言でプロジェクトの `env` の `DEVBASE_ACCOUNT_GROUP` を直すよう案内する。`list` の見出しは +`=== グローバル(グループ with) (...) ===` / `=== プロジェクト: web(グループ with) (...) ===` +の形になる(`version: 1` ではグループが付かない)。 + +#### dispatch 前の注入 + +`version: 2` の端末では、`cli._load_secret_env` が注入に使うプロジェクトを次のように決める。 + +| コマンド | dispatch 前の注入 | +| --- | --- | +| 名前を指定したライフサイクル操作(ショートカットと `project` のサブコマンドで、名前が `projects/` に実在する) | 指定したプロジェクトで解決する | +| `env` の `list` / `get` / `set` / `delete` / `edit` / `init` / `sync` / `project` / `export` / `import` | 行わない | +| それ以外(`env exec` など) | 実行時のディレクトリのプロジェクトで解決する | + +実行時のディレクトリで解決すると、`projects/api` から Python を直接起動した `up web`(TUI など)が +切替元 `api` のグループのパスへ要求し、別グループの機密をいったんホストのプロセスへ載せる。 +起動ラッパーは Python の前に `projects/` へ移るため、ラッパー経由では最初から指定した +プロジェクトで解決する。`version: 1` では実行時のディレクトリで解決する(パスがグループで +分かれず、取得するのは同じチームの置き場である)。`env` の 10 のサブコマンドは対象の参照を +自分で決め、値を環境変数から使わない。`--group` や `-p` の検証より前に注入すると、拒むはずの +操作でも別グループのパスへ要求が出る。 + +#### `up` / `scale` のグループの食い違い + +`version: 2` のとき、`up` と `scale` はボリュームのグループと機密のグループを比べ、食い違えば +両方の値と出所を述べて 1 で終わる(`container._check_group_consistency`)。グループの名前が +検証を通らないときも起動しない。`version: 1` では検査しない。 + +| 比べるもの | 決まり方 | +| --- | --- | +| ボリュームのグループ | `resolve_account_group()`(プロセスの環境変数 `DEVBASE_ACCOUNT_GROUP`、未設定なら `default`) | +| 機密のグループ | `groups.declare(root, 実行時のプロジェクト)`(読み替える前の名前で比べる) | + +| コマンド | 検査の位置 | 検査より後にある副作用 | +| --- | --- | --- | +| `up` | `_run_pre_up_checks` の冒頭(`_ensure_env_files` より前) | 子プロセスの `env init`、`pre-up` フック、自動スナップショット、ボリュームの作成、構成の生成 | +| `scale` | `cmd_scale` の冒頭(`project.local.yml` の `scale` を書き換える前) | `scale` の書き換え、ボリュームの作成、構成の生成 | + +グループを宣言していないプロジェクトで `DEVBASE_ACCOUNT_GROUP=kkg devbase up` と打つと、 +ラッパーが source する `env` に同じキーが無いため環境変数が残り、ボリュームは `kkg`、機密は +`default` になる(プロジェクトの `env` が宣言していれば、ラッパーの source が環境変数を上書き +するので食い違わない)。そのまま起動すると、あるグループのボリュームの認証で別のグループの +機密を使うコンテナができる。途中で止めると別グループの名前のボリュームや書き換えた `scale` が +残るため、副作用より前で検査する。`scale` は `_run_deploy_pipeline` を通らないため、同じ関数を +冒頭で呼ぶ。ボリュームの側を `declared_group` へ揃える形は、スナップショット・`status`・ +entrypoint へ渡す値まで経路が変わるため採らない。 + +`_ensure_env_files` は存在判定の参照に実行時のプロジェクトのグループを持たせ、共通機密が +未作成なら子プロセスの `env init` へ `--group <プロジェクトのグループ>` を渡す(`version: 2` の +ときだけ)。子プロセスは `cwd=$DEVBASE_ROOT` で起動して実行時のプロジェクトを持たず、渡さなければ +`$DEVBASE_ROOT/env` のグループの共通の参照へ書き、親が読み直す参照と揃わない。子プロセスの +`cwd` をプロジェクトへ変える形は、`env init` の収集器が `cwd` に依存しないことを確かめる範囲が +広がり、`version: 1` の挙動まで変わりうるため採らない。 + +#### `init` / `sync` / `project` / `export` / `import` + +扱うのはチーム単位の参照で、`version: 2` ではグループが次のように決まる。 + +| コマンド | グループ | +| --- | --- | +| `env init` | 対象のグループ(`--group` で指定できる)のチーム共通 | +| `env sync` | 対象のグループのチーム共通。同期済みハッシュの控えもそのグループのもの | +| `env project` | 実行時のプロジェクトのグループのチームのプロジェクト | +| `env export` | 共通は対象のグループ。プロジェクトは対象のグループと同じ置き場のものだけを集め、外したプロジェクトの名前とグループを標準エラーへ出す(その参照へは要求しない)。メタデータは対象のグループの控えを `env/sources.yml` として入れる | +| `env import` | 共通は対象のグループ、プロジェクトはそれぞれのグループ。バンドルに対象のグループと違う置き場のプロジェクトがあれば、サーバへ要求する前に名前とグループを挙げ、`--exclude-project` を案内して 1 件も取り込まずに 1(`--dry-run` も同じ)。`env/sources.yml` は対象のグループの控えへ取り込む | + +別グループのプロジェクトは、グループ単位のポリシーのサーバで 403 になる。`import` で黙って +飛ばすと取り込んだつもりの機密が欠けるため、止めて除外を案内する。 + +`env sync` はソースファイルのハッシュを控えに記録して変更を検出する。`version: 2` では控えを +置き場のグループごとに `$DEVBASE_ROOT/.env.sources..yml` へ分け、それ以外は +`$DEVBASE_ROOT/.env.sources.yml` 1 つである(`sources.sources_path`)。控えが 1 つだと、グループ A の +同期でハッシュが更新され、グループ B の同期が「変更なし」と判定されて B の置き場に古い +認証情報が残る。1 つのファイルの中をグループの節に分ける形は、古い devbase が節を知らずに +全体を書き戻すため採らない。 ### `devbase env backend` | コマンド | 入力 | 成功 | 失敗 | | --- | --- | --- | --- | | `status` | なし | backend 名、保存先、`mount`、4 参照のパス、個人単位の識別子、接続資格情報の有無と `role_id`、キャッシュの有無と最終取得時刻。0 | 設定が壊れていれば理由を述べて 1 | -| `use ` | `--url` `--mount` `--user ID` `--role-id` `--secret-id-stdin` `--cache` / `--no-cache` | 検証 → 資格情報の保存 → 設定の保存の順で行い、要約を表示(`secret_id` は伏せる)。0 | 未知の名前・必須項目の欠落は 2、鍵が無いなどは 1。**いずれも設定を書き換えない** | +| `use ` | `--url` `--mount` `--user ID` `--role-id` `--secret-id-stdin` `--cache` / `--no-cache` `--layout flat\|group` `--group-alias FROM=TO`(繰り返し可) | 検証 → 資格情報の保存 → 設定の保存の順で行い、要約(レイアウトと版、読み替えを含む)を表示(`secret_id` は伏せる)。0 | 未知の名前・必須項目の欠落・レイアウトの指定の誤り(後述)は 2、鍵が無いなどは 1。**いずれも設定を書き換えない** | | `test` | なし | 認証と参照ごとの取得(キャッシュへ落ちない)を行い、接続先 URL と読めた参照の件数を表示。0 | 到達できない・認証できない・サーバ backend でない → 1 | -| `migrate --to ` | `--to age\|openbao` `--dry-run` `--yes` | 後述 | 衝突は 2、読み戻しの不一致・書き込み失敗は 1 | +| `migrate --to ` | `--to age\|openbao` `--exclude-project NAME`(繰り返し可) `--dry-run` `--yes` | 後述 | 衝突・`projects/` に無い `--exclude-project` の名前は 2、読み戻しの不一致・書き込み失敗は 1 | + +`version: 2` の `status` は、置き場の前にレイアウトの行(`レイアウト: group (version 2)`)と +対象のグループの行(`グループ: default → nyle (projects/api/env にも $DEVBASE_ROOT/env にも宣言なし)` +の形。括弧は決めたファイル)を足し、4 参照のパスを対象のグループで組んで出す。プロジェクトの +外ではプロジェクトのパスを `/team//projects/` の形で出す。グループを決められ +なければ `グループ: 決められません (<理由>)` と出してパスを省く。`version: 1` とファイル backend +の出力にこれらの行は無い。 + +`version: 2` の `test` は、対象のグループの共通の参照と、対象のグループと同じ置き場の +プロジェクトの参照だけを調べ、外したプロジェクトの名前とグループを表示する(グループ単位の +ポリシーのサーバでは、別グループのプロジェクトの参照が正しい設定でも 403 になる)。 +`version: 1` では `projects/` の全プロジェクトを調べる。 + +`use` のレイアウトの決まり方: + +| 入力 | 結果 | +| --- | --- | +| `--layout` なし、既存の設定に `openbao` 節がある | 既存のレイアウトと読み替えを引き継ぐ | +| `--layout` なし、`openbao` 節が無い | `group`(`version: 2`)で書く | +| `--layout group` | `version: 2` で書く。`path_team_global` / `path_team_project_prefix` は捨てる | +| `--layout flat` | `version: 1` で書く。既存の `group_aliases` は捨て、捨てた旨を表示する | +| `--group-alias FROM=TO` | `group_aliases` をこの指定で置き換える(無ければ既存を引き継ぐ)。`FROM` と `TO` は `resolve_account_group` の規則と前後の空白の禁止で検証し、`TO` が `global` / `projects` なら拒む。同じ `FROM` を違う `TO` へ向ける指定も拒む | +| `--group-alias` と、`--layout flat` または(`--layout` なしで)既存の `version: 1` | 組み合わせの誤りとして 2 | +| `openbao` 以外の backend に `--layout` / `--group-alias` | 2 | +| `openbao` 以外の backend | 既存の版と `openbao` 節を引き継ぐ | +| 既存の設定からレイアウトが変わった | 設定を書いた後に `cache/` を消し、消した旨を表示する。消せなければ 1(設定は書き換え済みのまま) | + +レイアウトが変わると参照のパスが変わるため、古い控えが `scope` の不一致で使われることはない。 +それでも消すのは、別グループの機密が暗号文のまま残り続けるのを避けるためである。 `secret_id` は引数で受け取らない。`--secret-id-stdin` で標準入力の最初の行を読むか、TTY では 伏せ字入力で尋ねる。`use` は引数に無い項目を既存の設定から引き継ぎ、資格情報の指定が無ければ @@ -228,17 +453,18 @@ flowchart LR - KV v2 にはコメント・空行の置き場が無く、`load_bytes()` が返すのは `KEY=VALUE` の並び である -パスの対応: - -| 参照 | パス | -| --- | --- | -| チーム共通 | ``(既定 `team/global`) | -| チームのプロジェクト | `/`(既定 `team/projects/`) | -| 個人共通 | `//global`(既定 `users//global`) | -| 個人のプロジェクト | `//projects/` | +パスの対応(`` は参照のグループの置き場のグループ名): -`` と `` はパス区切りと `..` を含まない検査を通っているため、組み立てたパスが -設定した親の外へ出ることはない。URL へ埋め込む前に各要素を符号化する(`/` は区切りとして +| 参照 | `version: 1` | `version: 2` | +| --- | --- | --- | +| チーム共通 | ``(既定 `team/global`) | `//global`(既定 `team//global`) | +| チームのプロジェクト | `/`(既定 `team/projects/`) | `//projects/` | +| 個人共通 | `//global`(既定 `users//global`) | `///global` | +| 個人のプロジェクト | `//projects/` | `///projects/` | + +`` と `` はパス区切りと `..` を含まない検査を通っており、`` は +`DEVBASE_ACCOUNT_GROUP` と同じ名前の検査を通っているため、組み立てたパスが設定した親の外へ +出ることはない。URL へ埋め込む前に各要素を符号化する(`/` は区切りとして 残す)。`` は社内メールアドレスの `@` より前の部分で、サーバ側の entity 名と同じ値に する。サーバのポリシーは entity 名で個人単位のパスを絞るため、設定の `user` が本人と違えば `users//...` の取得が 403 になる。 @@ -246,6 +472,9 @@ flowchart LR サーバ側の構成(KV v2 のマウント、ポリシー、AppRole、token の期限)は devbase の範囲外で、 運用側のリポジトリ(carmo-cdk#312)が持つ。devbase が前提にするのは、上の 4 経路と 「本人のパスは読み書きでき、チームのパスは読め、他人のパスは拒まれる」ことだけである。 +ポリシーを `version: 2` のグループ単位に絞る変更も運用側のリポジトリの課題(carmo-cdk#363)が +扱う。devbase は 1 回の操作で対象のグループのパスだけを要求するため、グループ単位に絞った +サーバでも対象のグループの操作は通る。 ### `SecretStore` の持ち回り @@ -253,7 +482,7 @@ flowchart LR | 注入 | 理由 | | --- | --- | -| `cli._load_secret_env` | dispatch の前に現在地の機密を載せる(エディタ起動などが値を使う) | +| `cli._load_secret_env` | dispatch の前に現在地の機密を載せる(エディタ起動などが値を使う)。`version: 2` で名前を指定したときは指定したプロジェクトで解決する(「dispatch 前の注入」) | | `_dispatch_lifecycle`(名前を指定したとき) | 切替元の機密を落として切替先で載せ直す | | `_run_deploy_pipeline` | 起動の直前に必須として読む(鍵が無ければここで止める) | @@ -278,7 +507,8 @@ flowchart LR | 経路 | 認証 | 取得 | | --- | ---: | ---: | | `devbase up`(プロジェクト `web` の中) | 1 | 4 | -| `devbase up web`(別のプロジェクト `api` の中) | 1 | 6(`api` の 4 + 切替後の `web` 固有の 2) | +| `devbase up web`(別のプロジェクト `api` の中、`version: 1`) | 1 | 6(`api` の 4 + 切替後の `web` 固有の 2) | +| `devbase up web`(別のプロジェクト `api` の中、`version: 2`) | 1 | 4(`web` のグループの 4 だけ。`api` のグループのパスへは要求しない) | | `devbase up web`(`projects/` の外) | 1 | 4 | | 共通機密が未作成で `env init` を走らせた `up`(`up` のプロセスの分だけ。子プロセスの `env init` の往復は含まない) | 2 | 8 以下 | @@ -446,6 +676,23 @@ flowchart TD `--to openbao` でチーム単位のパスへ書く権限が無ければ、手順 2 で「書き込み権限が無い」旨を 述べて 1 で終了する(移行はチームの置き場を作る操作で、書ける利用者が行う)。 +移行の単位は、ファイル backend 側の参照(グループを持たない)と OpenBao 側の参照(`version: 2` +ではグループを持つ)の組である。グループは実行時のディレクトリに左右されず、共通の参照は +`ref_group(None)`(`$DEVBASE_ROOT/env` → `default`)、プロジェクトの参照は `ref_group()` で +決まる。 + +- `--exclude-project NAME` のプロジェクトは、読まない・書かない・退避しない(ファイルは元の + 位置に残る)。`projects/` に無い名前は、設定を読む前に名前を挙げて 2 で終了する(打ち間違いで + 移行してしまうのを防ぐ)。`version: 1` でも使える +- `version: 2` の要約(`--dry-run` を含む)は、参照ごとにサーバ上の `/<パス>` とキー名を + 出す。値は出さない +- `--to openbao` はプロジェクトごとのグループへ書く。書き込みの権限が無いグループのプロジェクトは + `--exclude-project` で外す +- `--to age` で移せる共通の参照は `secrets/global.env.age` 1 つだけなので、`$DEVBASE_ROOT/env` の + グループの共通の参照を移す。移すプロジェクトのグループのうち、それと違う置き場のグループの + 共通の参照には要求を出さず、グループ名とパスを表示してサーバ上に残す(移す機密が無いときも + 表示する) + 設定の書き換えを退避より先に行うのは、設定を書けなかったときに元のファイルだけが移動済みに なり、設定が指す先から機密が読めなくなるのを防ぐためである。`bootstrap.env.age` はどちらの 向きでも残す(再び `openbao` へ戻すときに資格情報を入れ直さずに済む)。サーバ側を消さない @@ -459,9 +706,9 @@ flowchart TD | `env init --reset` | ファイル backend では従来どおり `.backup` を複製する。サーバ backend では読み出した値を age で暗号化して `backups/env-init/<日時>/` へ控え、作れなければ 1 件も消さずに非ゼロで終了する | | `env encrypt` / `decrypt` | age 専用。backend が `openbao` なら止める。明示的な設定が変換後の保存先と逆(`plaintext` で `encrypt`、`age` で `decrypt`)でも止める(設定が指す先から機密が消える)。`auto` と一致する設定ではファイルの存在で判定する | | `env rekey` | backend の選択に関わらず実行でき、手元の age 暗号文すべて(機密の参照、`bootstrap.env.age`、`cache/` 配下)を 1 つのまとまりとして再暗号化する | -| `env export` | チーム単位の 2 種の参照だけを backend 越しに読む。個人単位のパスへ要求は届かない。バンドルの名前と `manifest.yml` の `version` は変わらない | +| `env export` | チーム単位の 2 種の参照だけを backend 越しに読む。個人単位のパスへ要求は届かない。バンドルの名前と `manifest.yml` の `version` は変わらない。`version: 2` のグループの扱いは「`init` / `sync` / `project` / `export` / `import`」 | | `env import` | チーム単位の参照へ backend 越しに書く。ファイル backend では従来どおり複製と原子的な rename。サーバ backend では現物を `fetch` で読んで merge の元と退避(age 暗号化して `backups/` へ全件)にし、参照ごとに `save_bytes()` する。失敗した参照までを控えた値で巻き戻す(結果が分からない参照は含め、サーバが拒んだと確定した参照は含めない)。サーバへの適用はローカルの計画(ファイル backend の参照、`--merge-metadata` の `sources.yml`)の確定より先に行い、サーバ側が失敗したときにメタデータだけが取り込み済みにならないようにする。受信者鍵が無ければ 1 件も取り込まない。暗号化の判定は「保存先が age か」で行い、`backend: age` で保存先がまだ無い参照も暗号文として保存する | -| `env doctor` | `backend.yml` の読み込みと登録簿の名前、`backend: openbao` でのブートストラップの 2 キー、`backend.yml` / `bootstrap.env.age` / `cache/` 配下の権限(ファイル `0600`、ディレクトリ `0700`)、`git check-ignore` による除外(`secrets/backend.yml` / `secrets/bootstrap.env.age` / `secrets/cache/team/global.env.age`)を点検する | +| `env doctor` | `backend.yml` の読み込みと登録簿の名前、`backend: openbao` でのブートストラップの 2 キー、`backend.yml` / `bootstrap.env.age` / `cache/` 配下の権限(ファイル `0600`、ディレクトリ `0700`)、`git check-ignore` による除外(`secrets/backend.yml` / `secrets/bootstrap.env.age` / `secrets/cache/team/global.env.age`)を点検する。`version: 2` では対象のグループの `.env.sources..yml` と `secrets/cache/team//global.env.age` の除外も点検する | ### 常に成り立つ条件 @@ -480,9 +727,19 @@ flowchart TD - サーバ backend への 1 つの参照の書き込みは、丸ごと反映されるか、何も反映されないかの どちらかである - 対応していない設定値は既定へ読み替えず、キー名と受け付ける値を添えて拒む。対象は - `version` が 1 以外、未知の backend 名、ループバック以外への `http`、パスやクエリを含む + `version` が 1 / 2 以外、未知の backend 名、ループバック以外への `http`、パスやクエリを含む `url`、パス区切りや `..` を含む `user` / `mount`、`/` で始まる・終わる・`..` を含む - `path_*` である + `path_*`、版に置けないキー(`version: 1` の `layout` / `path_team_prefix` / `group_aliases`、 + `version: 2` の `path_team_global` / `path_team_project_prefix`)、`version: 2` で `layout` が + 無い・`group` 以外、版と食い違う `layout`、`resolve_account_group` の規則を通らない・前後に + 空白がある・空の `group_aliases` のキーと値、読み替えた後が `global` / `projects` になる対応である +- `version: 2` の 1 回の操作(`up` / `scale` / `env` のコマンド / `env backend test`)がサーバへ + 要求するパスは、対象のグループの置き場のものだけである。`version: 1` とファイル backend では + 参照のグループが常に空で、パス・キャッシュの位置・往復の回数はグループを持たない参照と同じで + ある +- 参照のグループは非機密の `env` ファイルだけから決まり、機密の置き場の値とプロセスの環境変数は + 使わない。レイアウトと合わないグループの参照は、サーバへ要求する前に拒む +- `version: 2` の `up` / `scale` は、ボリュームと機密のグループが食い違ったまま副作用を起こさない ## データ・設定 @@ -503,17 +760,43 @@ cache: enabled: true ``` +`version: 2`(グループ別の置き場)の例: + +```yaml +version: 2 +backend: openbao +openbao: + url: https://openbao.example.com + mount: devbase + user: member01 + layout: group + path_team_prefix: team + path_user_prefix: users + group_aliases: + default: nyle + timeout_seconds: 5 +cache: + enabled: true +``` + | キー | 意味 | 空・不在のとき | | --- | --- | --- | -| `version` | 形式の版。`1` だけを受け付ける | `1` | +| `version` | 形式の版。`1`(レイアウト `flat`)と `2`(レイアウト `group`)を受け付ける | `1` | | `backend` | `auto` / `plaintext` / `age` / `openbao` | `auto` | | `openbao.url` | 接続先。ホスト(とポート)まで。パス・クエリ・フラグメントは不可。`https` に限り、`http` はホストが `localhost` / `127.0.0.1` / `::1` のときだけ受け付ける | `openbao` のとき必須 | | `openbao.mount` | KV v2 シークレットエンジンのマウント名。パス区切りと `..` は不可 | `devbase` | | `openbao.user` | 個人単位の置き場に使う識別子(entity 名)。パス区切りと `..` は不可 | `openbao` のとき必須 | -| `openbao.path_team_global` / `path_team_project_prefix` / `path_user_prefix` | パスの親。`/` で始めない・終えない | 上記の既定 | +| `openbao.layout` | `version: 2` だけに置き、`group` だけを受け付ける。`version: 1` には置けない(`flat` として動く) | `version: 2` では必須 | +| `openbao.path_team_global` / `path_team_project_prefix` | `version: 1` のチーム単位のパスの親。`/` で始めない・終えない。`version: 2` には置けない | 上記の既定 | +| `openbao.path_team_prefix` | `version: 2` のチーム単位のパスの親。`version: 1` には置けない | `team` | +| `openbao.path_user_prefix` | 個人単位のパスの親(両方の版) | `users` | +| `openbao.group_aliases` | グループ名 → 置き場のグループ名の対応(マッピング)。`version: 2` だけに置ける | 空 | | `openbao.timeout_seconds` | 1 回の HTTP の待ち時間(正の整数) | `5` | | `cache.enabled` | キャッシュを書く・読むか。偽なら既存の控えも消す | `true` | +`version: 2` は backend によらず `openbao.layout: group` を要する(節が無ければ `layout` の欠落として拒む)。 +設定の書き出しは、版に置けるキーだけを書く。 + ### `$DEVBASE_ROOT/secrets/bootstrap.env.age`(`0600`) `DEVBASE_OPENBAO_ROLE_ID` と `DEVBASE_OPENBAO_SECRET_ID` の `KEY=VALUE` を、機密の age @@ -522,11 +805,17 @@ cache: ### `$DEVBASE_ROOT/secrets/cache/`(ディレクトリ `0700`、ファイル `0600`) -| パス | 中身 | -| --- | --- | -| `team/global.env.age`、`team/projects/.env.age` | チーム単位の参照の控え | -| `user/global.env.age`、`user/projects/.env.age` | 個人単位の参照の控え(識別子はパスに入れず、`scope` で区別する) | -| `index.json` | 参照ごとの `fetched_at` / `backend` / `url_host`。`status` の表示だけに使い、可否の判定には使わない。キー名も値も入れない | +| パス(`version: 1`) | パス(`version: 2`) | 中身 | +| --- | --- | --- | +| `team/global.env.age`、`team/projects/.env.age` | `team//global.env.age`、`team//projects/.env.age` | チーム単位の参照の控え | +| `user/global.env.age`、`user/projects/.env.age` | `user//global.env.age`、`user//projects/.env.age` | 個人単位の参照の控え(識別子はパスに入れず、`scope` で区別する) | +| `index.json` | 同じ | 参照ごとの `fetched_at` / `backend` / `url_host`。`status` の表示だけに使い、可否の判定には使わない。キー名も値も入れない | + +`index.json` のキーは、`version: 1` で `team:global` / `team:project:` / `user:global` / +`user:project:`、`version: 2` で `team::global` / `team::project:` / +`user::global` / `user::project:` である。位置とキーは設定の `cache_relpath` / +`cache_key` が組む。グループごとに控えが分かれるため、グループの違うプロジェクトを順に起動しても +後の控えが先の控えを上書きせず、不達のときは各グループの控えで起動する。 控えは 1 参照 1 ファイルで、復号すると次の JSON になる。控えた機密と `scope` を同じ暗号文に 収め、原子的な置き換えで書くため、両者が食い違った組み合わせは残らない。KV v2 の版は @@ -537,6 +826,14 @@ cache: "fetched_at": "2026-09-08T10:00:00+09:00", "secrets": "KEY=value\n..."} ``` +### `$DEVBASE_ROOT/.env.sources.yml` / `.env.sources..yml` + +`env sync` の同期済みハッシュの控え(認証情報のソースファイルの位置とハッシュ、同期時刻)。 +`version: 2` では置き場のグループ名ごとに `.env.sources..yml` を持ち、それ以外は +`.env.sources.yml` 1 つである。機密の値は入らないが、ソースの位置を持つため `.gitignore` の +`.env.sources*.yml` で追跡から外す。`env export` はこの控えをバンドルの `env/sources.yml` に入れ、 +`env import` は対象のグループの控えへ戻す。 + ### 退避先 | 操作 | 退避先 | 形 | @@ -561,6 +858,11 @@ cache: できる。長期の資格情報はコンテナへ置かない - base イメージの `bao` は同じリリースの `checksums.txt` で検証する(署名は検証しない。 他のツールと同じ扱い) +- `version: 2` ではパスの先頭側でグループが分かれ、1 回の操作が組むパスは対象のグループのもの + だけになる。企業ごとのグループの機密を、別グループのプロジェクトへ届けずに済み、サーバは + グループ単位のポリシーで読み書きを絞れる +- グループを非機密の `env` ファイルから決めるため、機密の置き場に書いた値で読む置き場は + 変わらない。ボリュームのグループとの食い違いは `up` / `scale` が起動前に止める ## 運用 @@ -581,6 +883,19 @@ cache: - チーム単位のパスへ書けるのは、サーバ側で書き込みのポリシーを付けた利用者だけである。 それ以外の利用者の `env set`(`--user` なし)と `migrate --to openbao` は「書き込み権限が 無い」で止まる +- レイアウトは `devbase env backend use openbao --layout group|flat` で切り替える。devbase は + サーバ上のデータをレイアウトの間で移さず、前のレイアウトのパスの機密はサーバに残る。移し直す + 専用のコマンドは持たない(端末ごとに 1 回きりの作業で、チームのパスは 1 人が移せば済む)。 + 前のレイアウトのパスを `bao kv get` などで読み、`devbase env edit --group NAME`(個人単位は + `--user` も)で新しいパスへ書く。古いパスを版の履歴ごと消す操作(`kv metadata delete`)は + サーバ側の権限で決まり、devbase のコマンドは持たない +- グループの読み替え(`default` → 置き場のグループ名)は `backend.yml` の `group_aliases` 1 か所に + 書き、`env backend status` で確かめる +- `up` / `scale` がグループの食い違いで止まったら、起動したいグループに合わせて、プロジェクトの + `env` に `DEVBASE_ACCOUNT_GROUP` を書くか、シェルの環境変数を外す +- 機密の置き場に `DEVBASE_ACCOUNT_GROUP` を書くと、注入でプロセスの環境変数へ載ってボリュームの + グループを変えうる(プロジェクトの `env` が宣言していれば上書きされる)。`version: 2` ではこの + 場合も食い違いの検査で止まる。注入の対象から外すかは #185 で扱う。置き場には書かない ## テスト観点 @@ -625,7 +940,44 @@ cache: 接続先の適用、`--print`(`tests/commands/test_env_token.py`) - base イメージの `bao` の版・両アーキテクチャ・チェックサムの検証の文言 (`tests/containers/test_base_dockerfile_bao.py`) -- 実サーバに対する `devbase env backend test` / `devbase up` は手動確認。コンテナの中の +- 版とレイアウトの読み込み・書き出し、`version: 1` のパスとキャッシュの位置が変わらないこと、 + `version: 2` のパス・キャッシュの位置・接頭辞、置けないキー・`layout` の欠落と食い違い・ + `group_aliases` の名前と前後の空白・読み替え後の `global` / `projects` の拒否 + (`tests/env/test_backend_config.py`) +- グループの決まり方(プロジェクトの `env` → `$DEVBASE_ROOT/env` → `default`、空の値、`export` 付きと + 最後の行、プロセスの環境変数と置き場の値を使わないこと、名前の検証、出所の表示)、 + `SecretRef.group` の等価性と `label()`、`ref_group` / `storage_group` が `version: 1` とファイル + backend で `None` になること(`tests/env/test_groups.py`) +- レイアウトと合わないグループの参照の拒否と、グループのパスの読み書き(`tests/env/test_openbao.py`) +- グループごとのキャッシュのファイルと `index.json` のキー、不達で各グループの控えを使うこと、 + `version: 1` の控えをグループの参照に使わないこと(`tests/env/test_cache.py`) +- `resolve` が 4 参照へプロジェクトのグループを渡し、対象のグループのパスだけを要求すること + (`tests/env/test_runtime.py`) +- 控えのファイルがグループごとに分かれること(`tests/env/test_sources.py`)、`env sync` がグループ + ごとに変更を検出し、`env project` がプロジェクトのグループへ書くこと + (`tests/commands/test_env_sync_group.py`) +- `version: 2` の `up` の往復(プロジェクトの中・宣言なしの読み替え・別グループへの切替で認証 1 回・ + 取得 4 回、`env init` の子プロセスへの `--group` と書いた値での起動、`version: 1` では渡さない + こと)(`tests/cli/test_up_roundtrips.py`) +- dispatch 前の注入が名前を指定したライフサイクル操作で切替先を使い、`env` の 10 のサブコマンドで + 注入せず、`env exec` では注入すること(`tests/cli/test_secret_injection.py`) +- 下位ディレクトリからの `set` の宛先、`--group` の 5 コマンドの宛先と見出し、`-p` との食い違いの + 拒否(要求 0 回)と読み替え後の名前での比較、`list` / `get` がプロジェクトの参照を含めないこと、 + 使えない名前と `version: 1`・ファイル backend での 2(`tests/commands/test_env_user_axis.py`) +- `env init --group` の宛先・読み替え・拒否(`tests/commands/test_env_init_group.py`) +- `up` / `scale` がグループの食い違いで副作用より前に止まり、`version: 1` では止まらないこと + (`tests/commands/test_container_up_order.py`) +- `status` のレイアウト・グループ・出所・パス、`use` の `--layout` / `--group-alias` の引き継ぎ・ + 置き換え・拒否とキャッシュの削除、`test` を対象のグループへ絞ること + (`tests/commands/test_env_backend.py`) +- `migrate` のプロジェクトごとのグループ、`--exclude-project`(存在しない名前の 2、`version: 1`)、 + `--dry-run` のパス表示、`--to age` で他のグループの共通の参照へ要求せずに残すこと + (`tests/commands/test_env_backend_migrate.py`) +- `export` が対象のグループのプロジェクトと控えだけを集め、`import` が別グループのプロジェクトを + 含むバンドルを要求前に拒むこと(`tests/cli/test_env_bundle_backend.py`)、`doctor` がグループの + 控えとキャッシュの除外を点検すること(`tests/commands/test_env_ops_backend.py`) +- 実サーバに対する `devbase env backend test` / `devbase up` は手動確認。`version: 2` の + パスの読み書きと、プロジェクトのコンテナに別グループの機密が無いことも同じ。コンテナの中の `bao kv get` / `patch`、チーム共通への書き込みの 403、`env token` での取り直しも同じ。ポリシーによる 他人のパスと `users/` 直下の一覧の拒否、端末 1 台の `secret_id` の失効も同じ(開発モードの サーバでの確認結果は #166。本番のサーバでは配布後に確かめる) @@ -637,8 +989,11 @@ cache: - [CLI リファレンス: env](../user/cli-reference/03-env.md) - 発端の依頼: `issues/security-key.md` - 実装 PR: devbasex/devbase#171(Infisical 版 #167 を置き換え)、#177(`up` の往復、#168)、 - #178(コンテナの `bao`、#169) + #178(コンテナの `bao`、#169)、#184(アカウントグループごとの置き場、#182。設計は #183) - Infisical から OpenBao への切り替えの経緯: devbasex/devbase#166 +- サーバのポリシーをグループ単位に絞る課題: 運用側のリポジトリ(carmo-cdk#363) +- 範囲外として起票した課題: #185(機密の置き場に書いた `DEVBASE_ACCOUNT_GROUP` の注入) +- [環境変数ガイド: アカウントグループ](../user/environment-variables.md#アカウントグループ-devbase_account_group) - [OpenBao: KV v2 API](https://openbao.org/api-docs/secret/kv/kv-v2/) - [OpenBao: AppRole auth](https://openbao.org/docs/auth/approle/) - [OpenBao: Policies](https://openbao.org/docs/concepts/policies/) diff --git a/issues/PLAN56_secret-group-paths-decisions.md b/issues/PLAN56_secret-group-paths-decisions.md deleted file mode 100644 index 73013cbc..00000000 --- a/issues/PLAN56_secret-group-paths-decisions.md +++ /dev/null @@ -1,131 +0,0 @@ -# #182: 機密ストアの置き場をアカウントグループごとに分ける(決定の記録) - -設計文書 `issues/PLAN56_secret-group-paths-design.md` の「決定の記録」の節である。設計文書が -500 行を超えたため、この節だけを分けた。本文中の「決定 N」はこのファイルの見出しを指す。 - -## 決定の記録 - -### 決定 1: グループ別の置き場は `backend.yml` の `version: 2` にする - -古い devbase は未知のキーを黙って無視する(`_openbao_from_dict`)。キーを足すだけでは、 -配布が行き渡る前の端末が `version: 2` 相当の設定を読んで `team/global` を読み続け、別グループの -機密をコンテナへ渡す。版を上げれば、今の「`version` が 1 以外なら拒む」規則でその端末が止まる。 - -パスの設定に `{group}` の差し込みを許す案は採らない。個人単位のパスは `//…` を -コードが組んでおり、差し込みの位置を設定で表すにはキーを増やすことになる。古い devbase が -`{group}` を文字どおりのパスとして読む問題も残る。 - -### 決定 2: グループは `SecretRef` のフィールドとして持つ - -参照が自分のグループを持てば、`_seen` の鍵・キャッシュの位置・`label()` がすべて参照から -決まる。`up web` を `api` から打ったときのように、1 つの `SecretStore` の中でグループが -変わっても取り違えない。 - -グループを `SecretStore` のインスタンスに持たせる案は採らない。PLAN55 でストアはライフサイクル -操作 1 回の間持ち回られ、プロジェクトの切替をまたぐ。インスタンスのグループを書き換えると、 -控えの鍵がグループを含まず、切替元の値が返る。 - -### 決定 3: グループは非機密の `env` ファイルだけから決め、プロセスの環境変数を見ない - -ラッパーは実行時のディレクトリの `env` だけを読む。プロジェクトの下位ディレクトリから -打つと、プロジェクトの `env` がプロセスに載らない。`projects//env` を直接読めば、 -下位ディレクトリからでも同じグループになる(受け入れ条件 3)。置き場から読んだ値も -使わないため、置き場を決める値をその置き場から読む循環も起きない(受け入れ条件 4)。 - -グループを宣言していないプロジェクト(`$DEVBASE_ROOT/env` にも宣言なし)でシェルから -`DEVBASE_ACCOUNT_GROUP=kkg devbase up` と打つと、ラッパーが source する `env` に同じキーが無い -ため環境変数が残り、ボリュームは `kkg`、機密は `default` になり食い違う。プロジェクトの `env` が -宣言していれば、ラッパーの source が環境変数を上書きするので食い違わない。この食い違いは -決定 7 で起動を止めて知らせる。 - -### 決定 4: `default` の読み替えは `group_aliases` で置き場の上だけ行う - -`DEVBASE_ACCOUNT_GROUP` の既定値を `nyle` に変えると、ボリューム名が `devbase_home_default` -から変わり、既存の認証と会話ログのボリュームを移すことになる。devbase は公開リポジトリで、 -コードに社名を既定値として持ち込むことにもなる。読み替えを端末の設定に置けば、ボリュームに -触らず、社名はその会社の端末の設定にだけ入る。 - -### 決定 5: `version: 1` とファイル backend では参照のグループを常に空にする - -`ref_group()` が `None` を返せば、参照は今と同じ値になる。既存のテストの期待値・キャッシュの -位置・往復の数(受け入れ条件 9・10)が、分岐を足さずにそのまま保たれる。 - -グループを常に参照へ入れ、`version: 1` のパスの組み立てで無視する案は採らない。 -`_seen` の鍵にグループが入り、グループの違うプロジェクトへ切り替えたときに同じパスを -2 度取りに行く。 - -### 決定 6: `-p` と違うグループの `--group` は拒む - -`team/kkg/projects/web` に書いても、`web` のグループが `with` なら `up` はそこを読まない。 -書けたように見えて使われない機密が残る。プロジェクトのグループを変えたいなら -`projects/web/env` を直すのが筋で、それを文言で案内する。 - -### 決定 7: `layout: group` の `up` と `scale` はボリュームと機密のグループの食い違いで止める - -ボリュームは `resolve_account_group()`(プロセスの環境変数)、機密は `declared_group()` -(ファイル)で決まり、経路が 2 つある。食い違ったまま起動すると、`with` のボリュームの認証で -`nyle` の機密を使うコンテナができ、この変更で防ぎたい混ざり方がそのまま起きる。 - -ボリュームの側を `declared_group()` へ揃える案は採らない。スナップショット・`status`・ -entrypoint へ渡す値まで経路が変わり、この変更の範囲(前提 2)を超える。`version: 1` では -検査しない(今の起動を止めない)。`scale` は `_run_deploy_pipeline` を通らずにコンテナを -足すため、検査を共通の関数にして両方から呼ぶ。 - -### 決定 8: `env backend test` は対象のグループに属するプロジェクトだけを調べる - -今の `test` は `projects/` の全プロジェクトの参照を取りに行く。グループ単位のポリシーの -サーバでは、別グループのプロジェクトで 403 になり、正しい設定でも失敗に見える。対象の -グループ(実行時のプロジェクト、無ければ `$DEVBASE_ROOT/env`)に属するプロジェクトに絞る。 - -### 決定 9: 置き場を移し直すコマンドは作らない - -移し直しはグループ別の置き場へ切り替える端末ごとに 1 回で、チームのパスは 1 人が移せば -済む。`version: 1` の設定で `env get` / `bao kv get` で読み、`version: 2` の設定で -`env edit --group` で書けば足りる。版の履歴を消す操作(`kv metadata delete`)は権限が -管理者側にあり(carmo-cdk#340)、devbase のコマンドに入れても利用者の端末からは実行できない。 - -### 決定 10: `_ensure_env_files` は子プロセスの `env init` へ `--group` を渡す - -子プロセスは `cwd=$DEVBASE_ROOT` で起動する(`commands/container.py` の `_ensure_env_files`)。 -実行時のプロジェクトが無いため、共通の参照は `$DEVBASE_ROOT/env` のグループになる。`with` の -プロジェクトの `up` で `team/with/global` が空だと、子プロセスは `team/nyle/global` へ書き、 -親は読み直しても空のまま起動する。`--group` でプロジェクトのグループを渡せば、書く先と読む先が -揃う。 - -子プロセスの `cwd` をプロジェクトのディレクトリへ変える案は採らない。`env init` の収集器が -`cwd` に依存しないことを確かめる範囲が広がり、`version: 1` の端末の挙動まで変わりうる。 -`layout: group` でないときは `--group` を渡さない。 - -### 決定 11: 名前を指定したライフサイクル操作は、dispatch 前の注入から切替先で解決する - -`cli._load_secret_env` は dispatch の前に実行時のディレクトリのプロジェクトで注入する。 -`projects/api` から Python を直接起動した `up web`(TUI や `python -m devbase.cli`)では、 -切替元 `api` のグループのパスへ要求し、別グループの機密をいったんホストのプロセスへ載せる。 -`layout: group` で指定した名前が `projects/` に実在するときは、その名前で解決する。 - -`version: 1` には広げない。PLAN55 の往復の表とテストの期待値(受け入れ条件 9)が変わる。 -`version: 1` ではパスがグループで分かれず、取得するのは同じチームの置き場である。 - -### 決定 12: `export` は対象のグループのプロジェクトだけを集め、`import` は別グループのプロジェクトで止める - -今の `export` は既定で全プロジェクト、`import` はバンドル内の全プロジェクトを扱う。 -グループ単位のポリシーのサーバでは、別グループのパスで 403 になる。`export` は -`declared_group` が対象のグループと同じ置き場のプロジェクトだけを集め、外したプロジェクト名を -標準エラーへ出す。 - -`import` で別グループのプロジェクトを黙って飛ばすと、取り込んだつもりの機密が欠ける。 -名前とグループを挙げて 1 件も取り込まずに 1 で終了し、既存の `--exclude-project NAME`(繰り返し可)での -除外を案内する。 - -### 決定 13: `env sync` の同期済みハッシュは置き場のグループごとに持つ - -`SourcesManager` は `$DEVBASE_ROOT/.env.sources.yml` 1 つに、ソースファイルのハッシュを記録する。 -`env sync` の宛先だけをグループ別にすると、グループ A で同期した時点でハッシュが更新され、 -グループ B の同期は「変更なし」と判定される。B の置き場には古い認証情報が残る。 -`layout: group` では控えのファイル名に置き場のグループ名を入れ、`env sync` / `export` / `import` の -`--merge-metadata` が対象のグループの控えを読み書きする。控えは認証情報のソースの位置とハッシュを -持つため、今の `.env.sources.yml` と同じく Git の追跡から外す。`.gitignore` を `.env.sources*.yml` へ -広げ、`env doctor` の除外の点検に加える。 - -控えを 1 つのファイルの中でグループごとの節に分ける案は採らない。`version: 1` の端末と -同じファイルの形が変わり、古い devbase が読むと節を知らずに全体を書き戻す。 diff --git a/issues/PLAN56_secret-group-paths-design.md b/issues/PLAN56_secret-group-paths-design.md deleted file mode 100644 index 8dbbf485..00000000 --- a/issues/PLAN56_secret-group-paths-design.md +++ /dev/null @@ -1,417 +0,0 @@ -# #182: 機密ストアの置き場をアカウントグループごとに分ける(設計) - -要求と受け入れ条件は `issues/PLAN56_secret-group-paths.md` に、決定の理由は -`issues/PLAN56_secret-group-paths-decisions.md` にある。この文書は「どう作るか」だけを扱う。 - -作るものは 3 つである。 - -1. **グループを含むパスの並び**を `backend.yml` の `version: 2` として足す(決定 1) -2. **参照(`SecretRef`)にグループを持たせる**。グループは機密を読む前に非機密の `env` - ファイルから決める(決定 2・3) -3. **`default` の読み替え**を `backend.yml` の `group_aliases` に置く(決定 4) - -`version: 1` の設定とファイル backend では、参照のグループは常に空で、パス・キャッシュ・ -往復は今と同じになる(決定 5)。 - -## 機能一覧 - -| # | 機能 | 誰が使うか | -| --- | --- | --- | -| F1 | `devbase up` などの注入が、プロジェクトのアカウントグループの置き場だけを読む | 開発者(意識せずに使う) | -| F2 | `env list` / `get` / `set` / `delete` / `edit` が、実行したプロジェクトのグループを相手にする。`--group` で明示もできる | 開発者 | -| F3 | `default` などのグループ名を、置き場の上だけ別の名前へ読み替える | 端末の設定者 | -| F4 | `env backend status` が対象のグループとそのパスを表示する | 開発者 | -| F5 | `env backend use openbao --layout group --group-alias default=nyle` でグループ別の置き場へ切り替える | 端末の設定者 | -| F6 | `env backend migrate --to openbao` がプロジェクトごとのグループへ書き、`--exclude-project` で移行から外す | 端末の設定者 | -| F7 | 手元のキャッシュがグループごとに分かれ、不達のときに自分のグループの控えで起動する | 開発者(意識せずに使う) | -| F8 | 起動するボリュームのグループと機密のグループが食い違えば `up` / `scale` を止める | 開発者(誤設定の検出) | - -## 構成要素 - -| 要素 | 責務 | -| --- | --- | -| `env/groups.py`(新設) | `declared_group(root, project)`: `projects//env` → `$DEVBASE_ROOT/env` → `default` の順に `DEVBASE_ACCOUNT_GROUP` を**ファイルから**読み、`volume.manager.resolve_account_group` で検証する。プロセスの環境変数と機密の置き場は見ない(決定 3) | -| `env/backend_config.py`(変える) | `version: 2` を受け付ける。`OpenBaoSettings` に `layout`(`flat` / `group`)・`path_team_prefix`・`group_aliases` を足す。`storage_group(group)` が読み替えと予約語の検査を行う。`path_of(ref)` が `layout` でパスを組む。`cache_relpath(ref)` を足す | -| `env/secret_store.py` `SecretRef`(変える) | 末尾に `group: Optional[str] = None` を足す。`for_global` / `for_project` が `group=` を受ける。`label()` はグループがあれば `(グループ <名前>)` を後ろに付ける | -| `env/secret_store.py` `SecretStore`(変える) | `ref_group(project)` を足す。設定が `openbao` かつ `layout: group` のときだけ `declared_group` の結果を返し、それ以外は `None` | -| `env/openbao.py` `OpenBaoBackend`(変える) | `layout: group` で `ref.group` が空の参照を受けたら `SecretStoreError` で止める(呼び出しの誤りを従来のパスへ落とさない) | -| `env/cache.py`(変える) | `entry_path` / `entry_key` を設定の `cache_relpath` から組む。`layout: flat` では今と同じ位置 | -| `env/runtime.py` `resolve()`(変える) | `store.ref_group(project)` で得たグループを 4 参照に渡す | -| `commands/env.py`(変える) | `_global_env` / `_project_env` / `_target_env` がグループを決める。`--group` の検証と、`-p` とプロジェクトのグループの食い違いを拒む。`list` の見出しにグループを出す。`init` / `sync` / `project` / `export` / `import` も同じ決め方を使う | -| `env/sources.py` `SourcesManager`(変える) | 同期済みのハッシュの控えを置き場のグループごとに持つ。`layout: group` では `$DEVBASE_ROOT/.env.sources..yml`、それ以外は今の `.env.sources.yml`(決定 13) | -| `env/bundle.py` / `env/io_import.py`(変える) | 共通の参照は対象のグループ、プロジェクトの参照はそのプロジェクトのグループで作る。`export` は対象のグループに属するプロジェクトだけを集め、`import` はバンドルに別グループのプロジェクトがあれば 1 件も取り込まずに止める(決定 12) | -| `commands/env_backend.py`(変える) | `status` にグループの行と 4 パス。`use` に `--layout` / `--group-alias`。レイアウトが変わったらキャッシュを消す。`test` は対象のグループに属するプロジェクトだけ調べる。`migrate` に `--exclude-project` と、参照ごとのグループ | -| `commands/container.py` `_ensure_env_files` / `_run_pre_up_checks` / `cmd_scale`(変える) | 存在判定の参照にグループを渡す。子プロセスの `env init`(`cwd` は `$DEVBASE_ROOT`)へ `--group <プロジェクトのグループ>` を渡す(決定 10)。共通の検査 `_check_group_consistency(project)` でボリュームのグループ(`resolve_account_group()`)と `declared_group` を比べ、`layout: group` で食い違えば止める(決定 7)。呼ぶ位置は `_run_pre_up_checks` の冒頭(`_ensure_env_files` より前)と `cmd_scale` の冒頭 | -| `cli.py` `_load_secret_env`(変える) | `layout: group` で、名前を指定したライフサイクル操作(`up ` など)の dispatch 前の注入を、実行時のディレクトリではなく指定したプロジェクトで解決する(決定 11) | -| `commands/env_ops.py` `doctor`(変える) | `git check-ignore` で点検するキャッシュのパスを設定の `cache_relpath` から組む。`layout: group` では `.env.sources..yml` も点検する | -| `.gitignore`(変える) | `.env.sources.yml` の行を `.env.sources*.yml` に広げ、グループごとの控えを追跡対象から外す(決定 13) | -| `cli.py` の引数(変える) | `env list/get/set/delete/edit` と `env init` に `--group NAME`、`env backend use` に `--layout {flat,group}` と `--group-alias FROM=TO`(繰り返し可)、`env backend migrate` に `--exclude-project NAME`(繰り返し可) | -| 文書(変える) | `docs/user/env-backend.md`・`docs/user/environment-variables.md`「アカウントグループ」・`docs/user/cli-reference/03-env.md`。確定仕様 `docs/specifications/secret-backend.md` は `plan-to-spec` で | - -**`version` の値の集合へ `2` を足すため、`1` だけを前提にした既存の規則を集めた。** 当てはまらない -規則は次の 7 つで、いずれも上の表に載せた。 - -| 規則 | 置き場所 | -| --- | --- | -| `version != 1` の拒否 | `backend_config._from_dict` | -| 設定の書き出し | `backend_config.to_dict` | -| 既存設定の引き継ぎ | `env backend use` | -| キャッシュの位置と `index.json` のキー | `cache.entry_path` / `entry_key` | -| 除外を点検するキャッシュのパス | `env doctor` | -| パスの表示 | `env backend status` | -| 参照の列挙 | `env backend test` | - -`rekey` と `encrypt` / `decrypt` は当てはまるため変えない。`rekey` は手元の age 暗号文をパスに -よらず集め、`encrypt` / `decrypt` はファイル backend だけを扱う。 - -構成要素の関係: - -```mermaid -graph TD - subgraph cmd [コマンド] - ENV[commands/env.py] - EB[commands/env_backend.py] - CT[commands/container.py] - BUN[env/bundle.py / io_import.py] - end - subgraph env [env] - GR[groups.declared_group] - RT[runtime.resolve] - ST[SecretStore.ref_group] - REF[SecretRef.group] - CFG[backend_config
layout / group_aliases] - OB[OpenBaoBackend] - CA[cache] - end - VOL[volume.manager
resolve_account_group] - FILES[(projects/name/env
DEVBASE_ROOT/env)] - SRV[(OpenBao)] - ENV --> ST - EB --> ST - BUN --> ST - CT --> RT - CLI[cli._load_secret_env] --> RT - CT -->|食い違いの検査| VOL - CT --> GR - RT --> ST - ST --> CFG - ST --> GR - GR --> FILES - GR -->|名前の検証| VOL - ST --> REF - OB --> CFG - OB --> CA - CA --> CFG - OB -->|team/group/... だけ| SRV -``` - -図に含めない要素は次の 3 つである。 - -| 要素 | 含めない理由 | -| --- | --- | -| `cli.py` の引数 | 引数を定義し、上の各コマンドへ値を渡すだけ | -| `env_ops.py` の `doctor` | `backend_config.cache_relpath` を読むだけ | -| 利用者向け文書 | 実行時の関係を持たない | - -## 構造 - -```mermaid -classDiagram - class SecretRef { - +kind: str - +name: str | None - +owner: str - +group: str | None - +for_global(owner, group) SecretRef - +for_project(name, owner, group) SecretRef - +label() str - } - class SecretStore { - +config: BackendConfig - +ref_group(project) str | None - } - class OpenBaoSettings { - +layout: str - +path_team_prefix: str - +path_team_global: str - +path_team_project_prefix: str - +path_user_prefix: str - +group_aliases: dict - +storage_group(group) str - +path_of(ref) str - +cache_relpath(ref) str - } - class groups { - +declared_group(root, project) str - } - SecretStore --> OpenBaoSettings : config.openbao - SecretStore --> groups : layout group のとき - OpenBaoSettings --> SecretRef : パスを組む -``` - -`OpenBaoBackend._seen` の鍵は `SecretRef` のままでよい。`layout: group` ではグループが参照の -等価性に入るため、グループの違う同じ種類の参照を取り違えない。`layout: flat` ではグループが -常に `None` で、等価性は今と同じになる。 - -## データ構造 - -### `secrets/backend.yml`(`version: 2`) - -```yaml -version: 2 -backend: openbao -openbao: - url: https://openbao.example.com - mount: devbase - user: member01 - layout: group - path_team_prefix: team - path_user_prefix: users - group_aliases: - default: nyle - timeout_seconds: 5 -cache: - enabled: true -``` - -| キー | `version: 1` | `version: 2` | -| --- | --- | --- | -| `openbao.layout` | 置けない(`flat` として動く) | 必須。`group` だけを受け付ける。不在なら拒む | -| `openbao.path_team_global` / `path_team_project_prefix` | 今と同じ | 置けない(置けば拒む) | -| `openbao.path_team_prefix` | 置けない | チーム単位の親。既定 `team` | -| `openbao.path_user_prefix` | 今と同じ | 今と同じ(既定 `users`) | -| `openbao.group_aliases` | 置けない | グループ名 → 置き場のグループ名の対応。キーも値も `resolve_account_group` の検証を通す。既定は空 | - -**版とレイアウトを 1 対 1 にする。** `version: 1` は `flat`、`version: 2` は `group` である。 -`version: 2` で `layout` を必須にするのは、読み手が版の番号から並びを思い出さなくて済むためである。 -置けないキーを置いたときは、キー名と版を添えて拒む(今の「対応していない値は既定へ -読み替えない」規則に揃える)。 - -### パスの対応 - -`` は `storage_group(ref.group)`、すなわち `group_aliases` で読み替えた後の名前である。 - -| 参照 | `version: 1`(今と同じ) | `version: 2` | -| --- | --- | --- | -| チーム共通 | `team/global` | `//global` | -| チームのプロジェクト | `team/projects/` | `//projects/` | -| 個人共通 | `users//global` | `///global` | -| 個人のプロジェクト | `users//projects/` | `///projects/` | - -**置き場のグループ名に `global` と `projects` を使えない。** `team/projects/global` -(`version: 1` のプロジェクト `global`)と `team/projects/` の並びに、`version: 2` の -パスが重なるためである。読み替えの後の名前で検査する。 - -### キャッシュ - -| 参照 | `version: 1`(今と同じ) | `version: 2` | -| --- | --- | --- | -| チーム共通 | `cache/team/global.env.age` | `cache/team//global.env.age` | -| チームのプロジェクト | `cache/team/projects/.env.age` | `cache/team//projects/.env.age` | -| 個人共通 | `cache/user/global.env.age` | `cache/user//global.env.age` | -| 個人のプロジェクト | `cache/user/projects/.env.age` | `cache/user//projects/.env.age` | -| `index.json` のキー(チーム共通 / チームのプロジェクト / 個人共通 / 個人のプロジェクト) | `team:global` / `team:project:` / `user:global` / `user:project:` | `team::global` / `team::project:` / `user::global` / `user::project:` | - -控えの中身と `scope`(URL・`mount`・パス・`role_id` の SHA-256)は変えない。パスが変わる -ため、`version: 1` の控えを `version: 2` の参照に使うことは `scope` の不一致で起きない。 -それでも `use` でレイアウトが変わったら `cache/` を消す(別グループの機密が暗号文のまま -残り続けるのを避ける)。 - -## 入出力の契約 - -### `env list` / `get` / `set` / `delete` / `edit` - -```text -devbase env {set|delete|edit} [-p] [--user] [--group NAME] ... -devbase env list [-g|-p] [--user] [--group NAME] ... -devbase env get [--user] [--group NAME] KEY -devbase env init [--reset] [--group NAME] -``` - -`set` / `delete` / `edit` の `-p` は宛先を、`list` の `-p` はプロジェクトの節だけを出すことを -指す(今と同じ)。`get` は `-p` を取らず、プロジェクトの参照を実行時のディレクトリから -自動で含める。以下の「`-p` あり」は、`list -p` を含めた 4 コマンドに当たる。 - -| 状況 | 対象のグループ | 結果 | -| --- | --- | --- | -| `version: 2`、`--group` なし | `declared_group(root, 実行時のプロジェクト)` | そのグループの参照 | -| `version: 2`、`--group NAME`、`-p` なし | `NAME` | 共通の参照だけ `NAME` のもの | -| `version: 2`、`--group NAME`、`-p` あり、プロジェクトと同じ置き場 | `NAME` | そのグループのプロジェクトの参照 | -| `version: 2`、`--group NAME`、`-p` あり、プロジェクトと違う置き場 | — | 両方の名前を述べて 1。読み書きしない(決定 6) | -| `version: 2`、`-p` なしの `list` と `get` で `--group NAME`、プロジェクトと違う置き場 | `NAME` | 共通の参照だけを出す・探す。プロジェクトの参照は含めず、含めなかった旨を標準エラーへ 1 行出す | -| `--group` の名前が使えない | — | `resolve_account_group` と同じ理由(予約語・数字だけ・文字種)、または読み替えた後の名前が `global` / `projects` であることを述べて 2 | -| `version: 1` またはファイル backend で `--group` | — | 「グループ別の置き場を選んだ設定でだけ使える」旨を述べて 2 | - -- **「同じ置き場」は読み替えた後の名前(`storage_group`)で比べる。** `default: nyle` の対応が - あれば、宣言の無いプロジェクトで `-p --group nyle` も `-p --group default` も通る。 - 誤りの文言には読み替える前と後の両方を出す(`default → nyle`) -- `list` の各節の見出しは `グローバル(グループ with)` の形になる(`SecretRef.label()`)。 - `version: 1` では今と同じ -- 終了コード 2 は、今の `env` コマンドの引数の誤りと同じ扱いである - -### `env backend use` - -```text -devbase env backend use openbao [--layout {flat,group}] [--group-alias FROM=TO]... [既存の項目] -``` - -| 入力 | 結果 | -| --- | --- | -| `--layout` なし・既存の設定あり | 既存の版とレイアウトを引き継ぐ | -| `--layout` なし・既存の設定なし | `group`(`version: 2`)で書く | -| `--layout group` | `version: 2` で書く。`path_team_global` / `path_team_project_prefix` は捨てる | -| `--layout flat` | `version: 1` で書く。既存の `group_aliases` があれば捨てた旨を出す | -| `--group-alias` と、`--layout flat` または(`--layout` なしで)既存の設定が `version: 1` | 組み合わせの誤りとして 2。設定を書き換えない(`version: 1` の `--group` を 2 で拒むのと揃える) | -| `--group-alias default=nyle` | `group_aliases` を**この指定で置き換える**(1 つも無ければ既存を引き継ぐ)。`FROM` と `TO` は `resolve_account_group` の検証を通す。`TO` が `global` / `projects` のときも拒む(`FROM` は拒まない。`global` という名前のグループを別の置き場へ向ける対応は成り立つ)。通らなければ 2、設定を書き換えない | -| レイアウトが変わった | 設定を書いた後に `cache/` を消し、消した旨を出す | - -### `env backend status` - -`version: 2` では、今の出力の「置き場」の前に次の行を足す。4 つのパスは対象のグループで -組んだ実際のパスを出す。 - -```text - レイアウト: group (version 2) - グループ: default → nyle (projects/api/env にも $DEVBASE_ROOT/env にも宣言なし) -``` - -グループの行の括弧には、どのファイルで決まったかを出す。 - -### `env backend migrate` - -```text -devbase env backend migrate --to {openbao,age} [--exclude-project NAME]... [--dry-run] [--yes] -``` - -- 共通の参照は `declared_group(root, None)` で作る(`$DEVBASE_ROOT/env` → `default`) -- プロジェクトの参照は `declared_group(root, name)` で作る。どちらも実行時のディレクトリに - 左右されない -- `--exclude-project NAME` の参照は、読まない・書かない・退避しない。存在しないプロジェクト名は - 名前を述べて 2(打ち間違いで移行してしまうのを防ぐ) -- `--dry-run` は参照ごとに `/<パス>` と衝突したキー名を出す。値は出さない -- `--to age` は**移行元と移行先で参照を分けて作る**。移行元(`layout: group` の OpenBao)の参照は - 上の 2 行と同じ規則でグループを持ち、移行先(age)の参照はグループを持たない(ファイル backend は - 分けない)。移行元の参照をグループなしで作ると、`OpenBaoBackend` が空のグループを拒んで読めない -- `--to age` で移せる共通の参照は 1 つ(`secrets/global.env.age`)だけである。`$DEVBASE_ROOT/env` の - グループの共通の参照を移し、他のグループの共通の参照は移さない。プロジェクトの参照は、 - それぞれのプロジェクトのグループから移す -- 「他のグループ」は、移す対象のプロジェクト(`--exclude-project` で外したものを除く)の - `declared_group` のうち、`$DEVBASE_ROOT/env` のグループと違う置き場のものである。**その共通の - 参照へは要求を出さない。** 組み立てたパスを表示し、サーバ上に残す旨を述べるだけにする - (今の `--to age` がサーバ側を消さないのと同じ扱い) -- `migrate --to openbao` は全プロジェクトのグループへ書く。グループ単位のポリシーで書けない - グループのプロジェクトは `--exclude-project` で外す(外さなければ今と同じく「書き込み権限が - 無い」で止まる) - -### `devbase up` / `scale` の食い違いの検査 - -`layout: group` のときだけ、**副作用のある処理より前に**行う。 - -| コマンド | 検査の位置 | その後にある副作用 | -| --- | --- | --- | -| `up` | `_run_pre_up_checks` の冒頭(`_ensure_env_files` より前) | 子プロセスの `env init`、`pre-up` フック、自動スナップショット、ボリュームの作成、機密の注入、構成の生成 | -| `scale` | `cmd_scale` の冒頭(`project_runtime.write_scale` の前) | `project.local.yml` の `scale` の書き換え、ボリュームの作成、構成の生成 | - -途中で止めると、別グループの名前のボリュームや書き換えた `scale` が残るためである。 - -| 比べるもの | 食い違ったとき | -| --- | --- | -| ボリュームのグループ `resolve_account_group()`(プロセスの環境変数) ↔ `declared_group(root, project)`(ファイル) | 両方の値と、それぞれの出所を述べて 1。コンテナを起動しない | - -## 処理の流れ - -`devbase up web` を `projects/api`(グループ宣言なし → `default` → `nyle`)の中から打ち、 -`web` のグループが `with` のとき: - -```mermaid -sequenceDiagram - participant CLI as cli - participant RT as runtime - participant ST as SecretStore - participant GR as groups - participant OB as OpenBaoBackend - participant SRV as OpenBao - participant DL as _dispatch_lifecycle - participant PRE as _run_pre_up_checks - participant DEP as _run_deploy_pipeline - CLI->>RT: inject(root, "web")(指定した名前で解決。決定 11) - RT->>ST: ref_group("web") - ST->>GR: declared_group(root, "web") - GR-->>ST: with - RT->>OB: load 4 参照(group=with) - OB->>SRV: login + GET team/with/global ほか 3 - CLI->>DL: up web(chdir と env の載せ直し) - DL->>RT: clear_injected → inject(root, "web")(控えから返る) - DL->>PRE: 起動前の検査 - PRE->>GR: 冒頭で declared_group(root, "web") と resolve_account_group() を比べる - PRE->>PRE: _ensure_env_files(食い違わなかったときだけ) - DL->>DEP: スナップショットの後に起動 - DEP->>RT: inject(root, "web")(控えから返る) -``` - -往復は認証 1 回・取得 4 回になり、`api` のグループ(`nyle`)のパスへは要求しない。 -`version: 1` では dispatch 前の注入を今どおり実行時のディレクトリで解決する(PLAN55 の -往復の表のまま)。ラッパー経由の `devbase up web` は、Python の前に `projects/web` へ移るため、 -どちらの版でも最初から `web` で解決する。 - -`env set -p FOO=1` を `projects/web/src` で打ったとき: - -```mermaid -graph TD - A[env set -p FOO=1] --> B[current_project_name → web] - B --> C{--group はあるか} - C -->|なし| D[declared_group root web → with] - C -->|あり| E{プロジェクトのグループと同じか} - E -->|違う| X[両方の名前を述べて 1] - E -->|同じ| D - D --> F[SecretRef.for_project web group=with] - F --> G[fetch → 版を指定して save] - G --> H[team/with/projects/web] -``` - -## 非機能の実現方式 - -| 大項目 | 実現方式 | -| --- | --- | -| 性能・拡張性 | グループの決定はファイル 2 つを読むだけで、サーバへ往復しない。プロジェクト内の `up` は認証 1 回 + 取得 4 回のまま | -| 移行性 | `version: 1` の設定はパス・キャッシュの位置・参照の等価性が変わらない。`version: 2` を古い devbase が読むと、今の `version` の検査で拒まれ、従来のパスを読まない | -| セキュリティ | 参照はグループを持って作られ、1 回の操作が組むパスは対象のグループのものだけになる。`layout: group` でグループの無い参照を受けた `OpenBaoBackend` は止まる。ボリュームと機密のグループの食い違いで起動しない | -| 運用・保守性 | 読み替えは `group_aliases` の 1 か所。`status` がグループと出所とパスを出す | - -## 決定の記録 - -`issues/PLAN56_secret-group-paths-decisions.md` にある(決定 1〜13)。 - -## テスト設計 - -| 受け入れ条件 | 何で確かめるか | -| --- | --- | -| 1 | `tests/cli/test_up_roundtrips.py` に `version: 2` の場合を足し、偽サーバの要求のパスの一覧が 4 つ(`team/with/…` / `users//with/…`)だけで、認証が 1 回であること | -| 2 | 同上。宣言なし + `group_aliases: {default: nyle}` で `team/nyle/…`。生成される構成のボリュームが `devbase_home_default` | -| 3 | `tests/commands/test_env_user_axis.py` に、`projects/web/src` を実行時のディレクトリにした `set` / `set -p` / `set --user` の書き込み先 | -| 4 | `tests/env/test_groups.py`(新設)で、置き場に `DEVBASE_ACCOUNT_GROUP` があってもファイルの値を返すこと。`declared_group` はストアを受け取らない(シグネチャで固定) | -| 5 | `tests/commands/test_env_user_axis.py` に `$DEVBASE_ROOT` での `--group kkg` の 5 コマンドの宛先と `list` の見出し | -| 5a | 同上。`projects/web`(`with`)での `set -p --group kkg` が 1 で要求 0 回、`get --group kkg` がプロジェクトの参照を探さないこと。宣言の無いプロジェクトで `set -p --group nyle`(`default: nyle`)が通ること | -| 6 | 同上。使えない名前 4 つで終了コード 2 と偽サーバへの要求 0 回。`version: 1` とファイル backend での `--group` の拒否 | -| 7 | `tests/cli/test_up_roundtrips.py` の切替の場合に、グループの違う 2 プロジェクト。偽サーバへの要求に `team/nyle/…` / `users//nyle/…` が無く、認証 1 回・取得 4 回。`api` 固有のキーが残らない | -| 8 | `tests/env/test_cache.py` に、`nyle` と `with` の控えが別ファイルに置かれ、不達で各グループの控えが使われること | -| 9 | 既存の `tests/env/` / `tests/commands/` / `tests/cli/` が期待値を変えずに通ること。`tests/env/test_backend_config.py` に `version: 1` のパスの対応を固定する表を足す | -| 10 | 既存のファイル backend のテストが変更なしで通ること | -| 11 | `tests/commands/test_env_backend.py` に `status` のレイアウト・グループ・出所・4 パスの行 | -| 12 | `tests/commands/test_env_backend_migrate.py` に、グループの違う 2 プロジェクトと `--exclude-project` の場合、存在しない名前の 2、`--dry-run` がパスとキー名だけを出すこと。`version: 2` の OpenBao から `--to age` へ戻す場合に、`$DEVBASE_ROOT/env` のグループの共通の参照とプロジェクトごとのグループの参照が age へ移り、他のグループの共通の参照は移さずに名前とパスが表示され、そのパスへの要求が 0 回であること | -| 13 | `tests/cli/test_env_bundle_backend.py` に、`nyle` と `with` のプロジェクトがある `version: 2` で、`export` が要求するパスの一覧が対象のグループだけであること、`import` が別グループのプロジェクトを含むバンドルで 1 かつ要求 0 回であること。`env init` / `sync` / `project` の書き込み先 | -| 決定 13 | `tests/commands/` の `env sync` のテストに、`layout: group` で `nyle` と `with` を順に同期し、ソースファイルを更新した後の 2 回目もそれぞれの置き場へ書かれること。`version: 1` では `.env.sources.yml` を使うこと。`tests/commands/test_env_ops_backend.py` に、`doctor` が `.env.sources..yml` の除外を点検すること | -| 14 | 追加した出力を検査するテストで、偽サーバに置いた値と `secret_id` が標準出力・標準エラー・ログに現れないこと | -| 15 | `uv run pytest tests/`、`ruff check lib`、`python -m compileall -q lib bin` | -| 16 | `tests/commands/test_container_up_order.py` に、`layout: group` でボリュームとファイルのグループが違うと `up` と `scale` が 1 で終わり、スナップショット・ボリュームの作成・`project.local.yml` の書き換えが起きていないこと。`version: 1` では止めないこと | -| 17 | `tests/commands/test_env_backend.py` に、`nyle` と `with` のプロジェクトがある `projects/` で `test` を打ち、偽サーバへの要求が対象のグループのパスだけであること | -| 18 | `tests/cli/test_up_roundtrips.py` の `env init` を走らせる場合を `version: 2` と `with` のプロジェクトで行い、子プロセスの引数に `--group with` があり、書いた値でその `up` が起動すること | -| 決定 1 | `tests/env/test_backend_config.py` に、`version: 2` で `layout` が無いときと `path_team_global` を置いたときの拒否、`version: 1` で `group_aliases` を置いたときの拒否、読み替えた後の `global` / `projects` の拒否。`use --group-alias default=global` の 2 | - -## 未確認のまま残ること - -| 項目 | 内容 | -| --- | --- | -| 実サーバでの `version: 2` のパスの読み書き | 今のポリシー(`team/*` の読み取り、`users//*` の読み書き)がグループ別のパスを含むことを、リリース後テストで確かめる(前提 7) | -| 移し直しで古いパスの版の履歴を消す権限 | チーム単位のパスの `kv metadata delete` は、管理者も今は実行できない(carmo-cdk#340)。移し直しの `operation` の計画で扱う | -| ボリュームのグループがプロジェクトの下位ディレクトリで食い違う既存の挙動 | ラッパーが下位ディレクトリでプロジェクトの `env` を読まないため、今もボリュームのグループが共通の値になりうる。決定 7 の検査がこの場合も止めるかは、`up` を下位ディレクトリから打てるかに依存する。実装で確かめ、範囲外なら起票する | diff --git a/issues/PLAN56_secret-group-paths.md b/issues/PLAN56_secret-group-paths.md deleted file mode 100644 index 5bad1ace..00000000 --- a/issues/PLAN56_secret-group-paths.md +++ /dev/null @@ -1,383 +0,0 @@ -# PLAN56: 機密ストアのチーム共通と個人単位の置き場を、アカウントグループごとに分ける - -- 発端: #182 -- ワークフローモード: `standard` - - 根拠: 機密の置き場のパスと、`env` コマンド・`env backend`・`devbase up` の注入の振る舞いを - 変える。`env` コマンドのオプション(公開インタフェース)が増え、`secrets/backend.yml` の形も - 変わる。対象には `tests/env/test_backend_config.py` / `tests/env/test_openbao.py` / - `tests/env/test_cache.py` / `tests/env/test_runtime.py` / `tests/commands/test_env_user_axis.py` - / `tests/commands/test_env_backend_migrate.py` / `tests/cli/test_up_roundtrips.py` がある -- 閉じる課題: #182(この端末の置き場の移し直しは別の `operation` の Pull Request で行い、そこで閉じる) -- 設計: `issues/PLAN56_secret-group-paths-design.md` - -## 依頼(原文) - -> ## 何をするか -> -> 機密ストア(OpenBao backend)のチーム共通と個人単位の置き場を、ボリュームと同じくアカウントグループ(`DEVBASE_ACCOUNT_GROUP`)ごとに分ける。グループは `nyle` / `with` / `kkg` の 3 つ。 -> -> ## 背景 -> -> - ボリュームは `DEVBASE_ACCOUNT_GROUP` ごとに `devbase_home_` へ分かれている(`with-ai-dev` は `with`、`project-trygroup-prd` は `kkg`、未設定は `default` = 実質 nyle) -> - 一方、機密のチーム共通は `team/global` 1 つで、#172 の移行後は nyle の値(GCP プロジェクト、BigQuery のサービスアカウント鍵、Slack など)が入っている -> - そのため with / kkg のプロジェクトにも nyle の機密がいったん配られ、プロジェクトの `env` の空上書きで打ち消している(#152 と同じ、企業をまたいで混ざる形) -> - サーバのポリシーも「チーム共通を読める人は全グループの鍵を読める」ことになり、with / kkg だけの利用者を登録できない -> - #172 の単位 5 で `GCP_CREDENTIALS_BASE64__default`(nyle の BigQuery 用サービスアカウント鍵)を個人単位へ移したが、中身はチームの鍵だった。置き場を決める軸が足りないことの表れ -> -> ## 決まっていること(利用者、2026-09-15) -> -> - グループ名は `nyle` / `with` / `kkg`。いまの `default` は `nyle` として扱う -> - チーム共通だけでなく、個人単位(`users//…`)もグループで分ける(同じ人でも会社ごとに `GH_TOKEN` や AWS のプロファイルが違う) -> - v3.4.0 のマイルストーンで扱う(v3.4.0 は公開済みのため、配布は次の版) -> -> ## 未決(要求・設計の工程で決める) -> -> - `default` を `nyle` と読み替える方法(`DEVBASE_ACCOUNT_GROUP` の既定値を変えるのか、置き場だけ対応させるのか。ボリューム名 `devbase_home_default` との関係) -> - グループの外に置くもの(全グループ共通の機密)を持つか -> - ファイル backend(`age` / `plaintext`)でも分けるか -> - 移行の順序と、KV v2 の版の履歴に他グループの機密を残さない手順(#181 の記録にある教訓) - -(「変わるところ(見込み)」の表と「関連」は #182 の本文にある。ここへは写さない) - -## 目的 - -- `backend: openbao` の端末で、プロジェクトのコンテナへ届く機密を、そのプロジェクトの - アカウントグループの置き場のものだけにする。別グループの機密を空上書きで打ち消す運用を - 要らなくする -- サーバがグループ単位で読み書きを許せる形(パスの先頭側でグループが分かれる)にする。 - `devbase up` 1 回が、対象のグループ以外のパスへ要求を出さない -- 設定を変えない端末(`backend.yml` が今の形のまま、またはファイル backend)の挙動を変えない - -## 前提 - -- 前提 1: グループの分け方を使うかは `secrets/backend.yml` の設定で選ぶ。今の形の設定ファイルは - 書き換えなくても、今と同じパス(`team/global` など)を読み書きする。 - 成否の判定: 受け入れ条件 9 -- 前提 2: `DEVBASE_ACCOUNT_GROUP` の解決(`default` への既定を含む)とボリューム名 - `devbase_home_` は変えない。`default` を `nyle` と読むのは**置き場のパスだけ**で、 - その対応は `backend.yml` に書く(devbase のコードに社名を持ち込まない)。 - 成否の判定: 受け入れ条件 2 -- 前提 3: 全グループ共通の置き場は作らない。全グループで同じ値を使う機密は、グループごとの - 置き場へ同じ値を置く(共通の置き場を持つと、そこへ企業固有の機密が再び混ざる。置き場を - 足すのは後から足しても局所的に直せる)。成否の判定: 受け入れ条件 1 の「他のパスへ要求を - 出さない」 -- 前提 4: ファイル backend(`auto` / `age` / `plaintext`)はグループで分けない。1 台の端末の - 中に閉じており、サーバのポリシーという動機が当たらない。 - 成否の判定: 受け入れ条件 10 -- 前提 5: プロジェクトのアカウントグループは、機密を読む前に非機密設定(`projects//env`、 - `$DEVBASE_ROOT/env`)から決まる。機密の置き場に書いた `DEVBASE_ACCOUNT_GROUP` はグループの - 決定に使わない(置き場を決める値を、その置き場から読む循環になる)。 - 成否の判定: 受け入れ条件 3・4 -- 前提 6: この端末の既存の置き場(`team/global` 21 キー・`team/projects/*`・ - `users/takemi_ohama/*`)をグループ別のパスへ移し直し、古いパスを版の履歴ごと消す作業は、 - この変更の配布後に `operation` の Pull Request で行う。devbase に置き場を移し直す専用の - コマンドは作らない(1 回きりの作業で、`env` の `--group` と `bao kv` で足りる。足りなければ - その `operation` の計画で起票する)。成否の判定: 対象範囲の「含まない」 -- 前提 7: サーバ(carmo-cdk)のポリシーをグループ単位にする変更は carmo-cdk で行う。今の - ポリシー(`team/*` を読める・`users//*` を読み書きできる)はグループ別のパスも - そのまま含むため、devbase の変更はサーバの変更を待たずに配布できる。 - 成否の判定: 受け入れ条件 1 を今のサーバで確かめられること(リリース後テスト) - -## 対象範囲 - -含む: - -- グループを含む置き場のパスの組み立てと、それを選ぶ `backend.yml` の設定 -- `default` を置き場の上で別の名前へ対応させる設定 -- `runtime.resolve()`(`devbase up` / `scale` / `env exec` など注入の全経路)が、対象の - プロジェクトのアカウントグループのパスを読むこと。プロジェクトを切り替える経路を含む -- `env list` / `get` / `set` / `delete` / `edit` の対象グループの決め方と、グループを明示する - オプション -- `env init` / `sync` / `project` / `export` / `import` が扱うチーム単位の参照を、対象の - グループのものにすること -- `env backend status` の表示、`env backend use` での設定、`env backend test` が調べる参照、 - `env backend migrate --to openbao` でのグループの扱いと、移行からプロジェクトを外すオプション - (`test` は 2026-09-15 に追記。設計の決定 8) -- `devbase up` の起動の前に、ボリュームのグループと機密のグループの食い違いを検査すること - (2026-09-15 に追記。設計の決定 7) -- 手元のキャッシュ(`secrets/cache/`)と `env sync` の同期済みハッシュの控えをグループごとに - 分けること。控えを Git の追跡から外す `.gitignore` の更新と `env doctor` の点検(2026-09-15 に - 追記。設計の決定 13) -- 利用者向け文書(`docs/user/env-backend.md`・`docs/user/environment-variables.md`・ - `docs/user/cli-reference/03-env.md`)の更新と、`docs/specifications/secret-backend.md` への - 取り込み(`plan-to-spec`) - -含まない: - -- この端末の既存の置き場の移し直しと古いパスの削除(前提 6、別の `operation`) -- carmo-cdk のポリシーの変更(前提 7。carmo-cdk へ起票する) -- ファイル backend のグループ分け(前提 4) -- 全グループ共通の置き場(前提 3) -- `DEVBASE_ACCOUNT_GROUP` の既定値・ボリューム名・entrypoint の変更(前提 2) -- コンテナの中の `bao` と `env token` の変更(接続先と token は変わらず、パスを知っているのは - 利用者である。文書に新しいパスの例を足すだけにする) - -## 用語 - -| 用語 | 意味 | -| --- | --- | -| アカウントグループ(グループ) | `DEVBASE_ACCOUNT_GROUP` の解決結果。未設定なら `default`。ボリューム `devbase_home_` の単位 | -| 置き場のグループ名 | パスに入れる名前。グループ名と同じだが、`backend.yml` の対応で読み替えたもの(この端末では `default` → `nyle`) | -| グループ別の置き場 | パスにグループ名を含む置き場。例: `team//global`、`users///projects/` | -| 従来の置き場 | 今のパス(`team/global` など)。グループ別の置き場を選ばない設定で使う | -| 対象のグループ | 1 回の操作が読み書きするグループ。プロジェクトが決まればそのプロジェクトのグループ | - -## 受け入れ条件 - -グループ別の置き場を選んだ設定(`default` → `nyle` の対応あり)と偽 OpenBao サーバを前提とする -条件は、その旨を「前提」に書く。パスの例は設計で決める既定の並びを使い、並びが変われば -設計の決定に合わせて書き換える(書き換えた事実を残す)。 - -- [ ] 1. 前提: グループ別の置き場。`projects/web/env` に `DEVBASE_ACCOUNT_GROUP=with` - 操作: `projects/web` の中で `devbase up`(docker の呼び出しは差し替える) - 結果: 偽サーバへの取得が `team/with/global` / `users//with/global` / - `team/with/projects/web` / `users//with/projects/web` の 4 回だけで、認証は 1 回。 - それ以外のパス(`team/nyle/…`・従来の置き場を含む)への要求が 0 回 -- [ ] 2. 前提: グループ別の置き場。`projects/api/env` にも `$DEVBASE_ROOT/env` にも - `DEVBASE_ACCOUNT_GROUP` が無い - 操作: `projects/api` の中で `devbase up` - 結果: 取得するパスが `team/nyle/…` / `users//nyle/…` の 4 つ。生成される構成の - グループのボリュームは `devbase_home_default` のまま -- [ ] 3. 前提: グループ別の置き場。`projects/web/env` に `DEVBASE_ACCOUNT_GROUP=with` - 操作: `projects/web/src`(下位ディレクトリ)で `devbase env set FOO=1`、 - `devbase env set -p FOO=1`、`devbase env set --user FOO=1` - 結果: 書き込み先がそれぞれ `team/with/global`、`team/with/projects/web`、 - `users//with/global` -- [ ] 4. 前提: グループ別の置き場。`team/with/global` に `DEVBASE_ACCOUNT_GROUP=nyle` が入っている - 操作: 条件 1 と同じ `devbase up` - 結果: 取得するパスは条件 1 と同じ(置き場に書いた値でグループが変わらない) -- [ ] 5. 前提: グループ別の置き場 - 操作: `$DEVBASE_ROOT`(プロジェクトの外)で `devbase env list --group kkg`、`env get --group kkg KEY`、 - `env set --group kkg KEY=v`、`env delete --group kkg KEY`、`env edit --group kkg` - 結果: 読み書きの対象が `team/kkg/global`(`--user` を付ければ `users//kkg/global`)。 - 一覧の見出しにグループ名が出る。 - ~~`-p` を付ければ実行時のプロジェクトの `team/kkg/projects/`~~ → 条件 5a へ分けた - (2026-09-15、設計の決定 6。`$DEVBASE_ROOT` では `-p` がそもそも使えない) -- [ ] 5a. 前提: グループ別の置き場(`default: nyle` の対応あり)。`projects/web/env` に - `DEVBASE_ACCOUNT_GROUP=with`、`projects/api/env` に宣言なし - 操作: `projects/web` で `env set -p --group kkg FOO=1` と `env get --group kkg FOO`。 - `projects/api` で `env set -p --group nyle FOO=1` - 結果: `web` の `set -p` は両方のグループ名を述べて非ゼロで終了し、サーバへ要求を出さない。 - `web` の `get` は `team/kkg/global` / `users//kkg/global` だけを探す。`api` の `set -p` は - `team/nyle/projects/api` へ書く(読み替えた後の名前で同じ置き場と判定する) -- [ ] 6. 操作: `--group` に使えない名前(`ubuntu`、`1`、`bad name`、`a/b`)を渡す - 結果: 1 件も読み書きせず、`DEVBASE_ACCOUNT_GROUP` の検証と同じ理由を述べて非ゼロで - 終了する。従来の置き場の設定とファイル backend で `--group` を渡しても、黙って無視せず - 非ゼロで終了する -- [ ] 7. 前提: グループ別の置き場。プロジェクト `api`(グループ `nyle`)の中 - 操作: `devbase up web`(`web` のグループは `with`) - 結果: 認証 1 回。起動時の環境変数に `api` の 4 参照だけにあるキーが残っていない。 - ~~取得は `team/with/…` / `users//with/…` の 4 パスを含み、`web` の起動に - `nyle` の置き場の値が使われない~~ → 取得は `team/with/…` / `users//with/…` の - 4 パスだけで、`nyle` の置き場へ要求しない(2026-09-15、目的の「対象のグループ以外の - パスへ要求を出さない」に揃えた。設計の決定 11)。Python を直接起動する経路(TUI など)でも同じ -- [ ] 8. 前提: グループ別の置き場。`nyle` のプロジェクトと `with` のプロジェクトをそれぞれ 1 回 - `devbase up` した後、サーバへ到達できなくする - 操作: 2 つのプロジェクトで順に `devbase up` - 結果: どちらも自分のグループの控えで起動する(後から起動した方の控えが先の控えを - 上書きしていない)。一方のグループの控えが他方のプロジェクトに使われない -- [ ] 9. 前提: 今の形の `backend.yml`(グループ別の置き場を選ぶ設定が無い) - 操作: `devbase up`、`env list` / `get` / `set` / `delete` / `edit`、`env backend status` - 結果: 読み書きするパスとキャッシュのファイルの位置が変更前と同じ。既存のテストが - 期待値を変えずに通る -- [ ] 10. backend が `auto` / `age` / `plaintext` のとき、`up` の注入結果・`env` コマンドの - 出力と保存先・`env backend status` の出力が変更前と同じ(既存のテストが変更なしで通る) -- [ ] 11. 前提: グループ別の置き場 - 操作: `devbase env backend status`(`projects/web` の中と `$DEVBASE_ROOT` で) - 結果: 対象のグループ名(読み替えがあれば `default → nyle` の形)と、そのグループの - 4 参照のパスが出る -- [ ] 12. 前提: ファイル backend で、`secrets/global.env.age` と `secrets/projects/web.env.age` - (`web` は `with`)と `secrets/projects/csc.env.age` がある。移行先はグループ別の置き場 - 操作: `devbase env backend migrate --to openbao --exclude-project csc` - 結果: 共通は `team/nyle/global`、`web` は `team/with/projects/web` へ書かれる。`csc` の - 参照はサーバへ書かれず、ファイルは退避されずに元の位置に残る。`--dry-run` は書き先の - パスとキー名を出し、値を出さない。 - 逆向き(グループ別の置き場の OpenBao から `--to age`)では、`$DEVBASE_ROOT/env` のグループの - 共通の参照と各プロジェクトのグループのプロジェクトの参照が age へ移り、他のグループの共通の - 参照は移さずにグループ名とパスを表示する(2026-09-15 に追記。設計の「`env backend migrate`」) -- [ ] 13. `env init` / `sync` / `project` / `export` / `import` は、対象のグループのチーム単位の - 参照だけを読み書きし、他のグループのパスへ要求を出さない。`export` は対象のグループに - 属するプロジェクトだけを集め、外したプロジェクト名を出す。`import` はバンドルに別グループの - プロジェクトがあれば 1 件も取り込まずに名前とグループを挙げて非ゼロで終了する - (2026-09-15 に追記。設計の決定 12)。`env sync` は同期済みのハッシュを置き場のグループごとに - 持ち、グループ A で同期した後でもグループ B の同期が変更を検出する(2026-09-15 に追記。 - 設計の決定 13) -- [ ] 14. 機密の値・`secret_id`・token が、追加した出力(`status` のグループの行、`--dry-run`、 - 誤りの文言)に載らない -- [ ] 15. `uv run pytest tests/` が全件通り、`ruff check lib` と `python -m compileall -q lib bin` - が変更前と同じ結果 -- [ ] 16. 前提: グループ別の置き場。~~`projects/web/env` に `DEVBASE_ACCOUNT_GROUP=with`~~ → - `projects/api/env` にも `$DEVBASE_ROOT/env` にも `DEVBASE_ACCOUNT_GROUP` が無い(2026-09-15。 - 宣言のあるプロジェクトではラッパーの source が環境変数を上書きし、食い違いが起きない) - 操作: ~~`projects/web` で~~ `projects/api` で `DEVBASE_ACCOUNT_GROUP=kkg devbase up`(ボリュームの - グループが `kkg`、機密のグループが `default`)と、同じ環境変数での `devbase scale 2` - 結果: どちらも両方のグループ名と出所を述べて非ゼロで終了し、コンテナ・ボリューム・ - スナップショットを作らず、子プロセスの `env init` を起動せず、`project.local.yml` の `scale` を書き換えない。今の形の - `backend.yml` では同じ操作で止めない(2026-09-15 に追記。設計の決定 7) -- [ ] 17. 前提: グループ別の置き場。`projects/` に `nyle` と `with` のプロジェクトがある - 操作: `projects/web`(`with`)で `devbase env backend test` - 結果: 偽サーバへの要求が `with` の置き場のパスだけで、`nyle` のプロジェクトの参照を - 調べない(2026-09-15 に追記。設計の決定 8) -- [ ] 18. 前提: グループ別の置き場。`team/with/global` が未作成で、`env init` の子プロセスは - 偽サーバへ `INIT_KEY=value` を保存して成功終了するものに差し替える - 操作: `projects/web`(`with`)で `devbase up` - 結果: 子プロセスが `team/with/global` へ書き、その `up` のコンテナへ `INIT_KEY` が渡る - (2026-09-15 に追記。設計の決定 10) - -## 非機能の条件 - -| 大項目 | 条件 | -| --- | --- | -| 性能・拡張性 | `devbase up` 1 回の往復は変更前と同じ(プロジェクト内で認証 1 回 + 取得 4 回)。グループの決定でサーバへの往復を足さない | -| 移行性 | 今の形の `backend.yml` とキャッシュはそのまま使える。グループ別の置き場へ切り替えた後に今のキャッシュが別グループの控えとして使われない。古い devbase がグループ別の置き場の設定を読んだとき、従来の置き場を黙って読まずに止まる | -| セキュリティ | 1 回の `up` / `env` 操作が要求するパスは対象のグループのものだけ(サーバがグループ単位のポリシーで拒んでも、対象のグループの操作は通る) | -| 運用・保守性 | グループの読み替え(`default` → `nyle`)は `backend.yml` の 1 か所にだけ書き、`status` で確かめられる | - -## 影響 - -| 対象 | 影響 | -| --- | --- | -| 公開インタフェース | 変わる。`env list` / `get` / `set` / `delete` / `edit` に `--group`、`env backend migrate` に `--exclude-project` が増える。`backend.yml` に項目が増える。既存のオプションと既存の設定ファイルの意味は変えない | -| データ | サーバ上のパスの並びが変わる(選んだ端末だけ)。既存のパスの内容は devbase が移さない(前提 6) | -| 既存の振る舞い | グループ別の置き場を選んだ端末で、注入・`env` コマンド・キャッシュの位置が変わる。選ばない端末は変わらない | - -## 検証手段 - -| 項目 | 手段 | -| --- | --- | -| テスト | `uv run pytest tests/`(偽 OpenBao サーバは `tests/conftest.py`) | -| 静的解析 | `ruff check lib` / `python -m compileall -q lib bin` | -| 手動確認 | リリース後に、この端末で `operation`(前提 6)の後に `projects/with-ai-dev` と nyle のプロジェクトで `devbase env backend status` と `devbase up` を行い、`env exec -- env` に別グループの機密が無いことを確かめる(`release-verification`) | - -## 前提とする取り決め - -| 項目 | 参照先 / 決めたこと | -| --- | --- | -| プロジェクト構造 | 機密の置き場は `lib/devbase/env/`、コマンドは `lib/devbase/commands/`、引数は `lib/devbase/cli.py`。グループ名の検証は `lib/devbase/volume/manager.py` の `resolve_account_group` を再利用し、同じ検証を別の場所へ写さない | -| コーディング規約 | 既存のモジュールの docstring の密度と日本語の説明に合わせる。`ruff check lib` | -| テスト戦略 | パスの組み立てと設定の検証は単体(`tests/env/`)、`env` コマンドの宛先は CLI 層(`tests/commands/`)、`up` の往復とグループの切替は偽サーバを使う結合(`tests/cli/`) | - -## 境界 - -| 区分 | 内容 | -| --- | --- | -| 常に行う | 既存テストの実行、`ruff`、設定を変えない端末の互換の確認 | -| 確認してから行う | サーバ上の既存のパスへの書き込み・削除(この変更の中では行わない)、`DEVBASE_ACCOUNT_GROUP` の既定値の変更 | -| 行わない | carmo-cdk の変更、ファイル backend の構造変更、置き場を移し直す専用コマンドの追加 | - -## 未決 - -| 項目 | 誰が決めるか | 期限 | -| --- | --- | --- | -| ~~`backend.yml` でグループ別の置き場を選ぶ形とパスの並び~~ → `version: 2` とパスの対応表(設計の決定 1、2026-09-15) | 設計 | 決定済み | -| ~~`default` の読み替えの書き方~~ → `openbao.group_aliases`(設計の決定 4) | 設計 | 決定済み | -| ~~`-p` と `--group` の組み合わせ~~ → プロジェクトのグループと違えば拒む(設計の決定 6) | 設計 | 決定済み | -| 前提 2〜4(`default` は置き場だけ読み替える・全グループ共通の置き場を持たない・ファイル backend は分けない) | 利用者(設計 Pull Request の承認で確定) | 設計 Pull Request のマージ | - -## 実装計画 - -設計は `issues/PLAN56_secret-group-paths-design.md`、決定の理由は -`issues/PLAN56_secret-group-paths-decisions.md` にある。タスクは機能の単位で分け、各タスクの -終わりに `uv run pytest tests/` を通す(既存テストの期待値は変えない。受け入れ条件 9・10)。 - -### 修正対象 - -- `lib/devbase/env/groups.py`(新設)、`backend_config.py`、`secret_store.py`、`openbao.py`、 - `cache.py`、`runtime.py`、`sources.py`、`bundle.py`、`io_import.py` -- `lib/devbase/commands/env.py`、`env_backend.py`、`container.py`、`env_ops.py` -- `lib/devbase/cli.py`、`.gitignore` -- `docs/user/env-backend.md`、`docs/user/environment-variables.md`、`docs/user/cli-reference/03-env.md` -- テスト: `tests/env/`、`tests/commands/`、`tests/cli/` - -### Task 1: `version: 2` の設定とグループ付きの参照 - -- **対象ファイル:** `env/groups.py`、`env/backend_config.py`、`env/secret_store.py`、`env/openbao.py`、 - `env/cache.py`、`tests/env/test_groups.py`(新設)、`tests/env/test_backend_config.py`、 - `tests/env/test_cache.py` -- **変更内容:** `declared_group`、`OpenBaoSettings` の `layout` / `path_team_prefix` / - `group_aliases` / `storage_group` / `cache_relpath`、`version: 2` の検証(`layout` 必須、置けない - キーの拒否、`global` / `projects` の拒否)、`SecretRef.group`、`SecretStore.ref_group`、 - `OpenBaoBackend` の空グループの拒否、キャッシュの位置と `index.json` のキー -- **満たす受け入れ条件:** 4・8・9、決定 1〜5 -- **進め方:** 失敗するテスト → 最小実装 → 整理 - -### Task 2: 注入がプロジェクトのグループの置き場を読む - -- **対象ファイル:** `env/runtime.py`、`cli.py`(`_load_secret_env`)、`tests/cli/test_up_roundtrips.py`、 - `tests/env/test_runtime.py` -- **変更内容:** `resolve()` が `store.ref_group(project)` を参照へ渡す。`layout: group` で名前を - 指定したライフサイクル操作の dispatch 前の注入を切替先で解決する(決定 11) -- **満たす受け入れ条件:** 1・2・7 -- **進め方:** 偽サーバの要求のパスを数えるテストを先に書く - -### Task 3: `env` コマンドの対象グループと `--group` - -- **対象ファイル:** `commands/env.py`、`cli.py`、`tests/commands/test_env_user_axis.py` -- **変更内容:** `--group`(`list` / `get` / `set` / `delete` / `edit` / `init`)、読み替え後の名前での - 比較、`-p` との食い違いの拒否、`-p` なしの `list` / `get` がプロジェクトの参照を含めない場合、 - 見出しのグループ名、`version: 1` とファイル backend での拒否 -- **満たす受け入れ条件:** 3・5・5a・6・14 -- **進め方:** 失敗するテスト → 最小実装 → 整理 - -### Task 4: `up` / `scale` の食い違いの検査と `env init` への `--group` - -- **対象ファイル:** `commands/container.py`、`tests/commands/test_container_up_order.py`、 - `tests/cli/test_up_roundtrips.py` -- **変更内容:** `_check_group_consistency` を `_run_pre_up_checks` の冒頭と `cmd_scale` の冒頭で - 呼ぶ。`_ensure_env_files` の存在判定の参照にグループ、子プロセスへ `--group` -- **満たす受け入れ条件:** 16・18 -- **進め方:** 副作用が起きないことを先にテストで固定する - -### Task 5: `env backend status` / `use` / `test` - -- **対象ファイル:** `commands/env_backend.py`、`cli.py`、`tests/commands/test_env_backend.py` -- **変更内容:** `status` のレイアウト・グループ・出所・4 パス、`use --layout` / `--group-alias` - (組み合わせの拒否、レイアウトが変わったらキャッシュを消す)、`test` を対象のグループへ絞る -- **満たす受け入れ条件:** 11・17、決定 1 -- **進め方:** 失敗するテスト → 最小実装 → 整理 - -### Task 6: `env backend migrate` のグループと `--exclude-project` - -- **対象ファイル:** `commands/env_backend.py`、`cli.py`、`tests/commands/test_env_backend_migrate.py` -- **変更内容:** 参照ごとのグループ、`--exclude-project`、`--dry-run` のパス表示、`--to age` で - 移行元と移行先の参照を分け、他グループの共通の参照へ要求しない -- **満たす受け入れ条件:** 12 -- **進め方:** 失敗するテスト → 最小実装 → 整理 - -### Task 7: `init` / `sync` / `project` / `export` / `import` と同期済みハッシュ - -- **対象ファイル:** `commands/env.py`、`env/bundle.py`、`env/io_import.py`、`env/sources.py`、 - `commands/env_ops.py`、`.gitignore`、`tests/cli/test_env_bundle_backend.py`、 - `tests/commands/test_env_ops_backend.py` -- **変更内容:** 対象グループの参照、`export` のプロジェクトの絞り込み、`import` の拒否、 - `.env.sources..yml`、`doctor` の点検、`.gitignore` の `.env.sources*.yml` -- **満たす受け入れ条件:** 13、決定 12・13 -- **進め方:** 失敗するテスト → 最小実装 → 整理 - -### Task 8: 利用者向け文書 - -- **対象ファイル:** `docs/user/env-backend.md`、`docs/user/environment-variables.md`、 - `docs/user/cli-reference/03-env.md` -- **変更内容:** `version: 2` の設定例、パスの対応、`--group` / `--layout` / `--group-alias` / - `--exclude-project`、食い違いで止まったときの直し方 -- **満たす受け入れ条件:** 対象範囲の文書の項目 -- **進め方:** テスト駆動を適用しない(文書のみ) - -### リスクと対処 - -| リスク | 対処 | -| --- | --- | -| `commands/env.py`(1082 行)を Task 3・7 が触る | タスクごとにテストを通す。構造の整理は構造改善の工程へ回す | -| `SecretRef` の等価性が変わり、`version: 1` の往復やキャッシュが変わる | 決定 5 のとおりグループを `None` に保ち、既存テストの期待値を変えないことで検出する | - -### 切り戻し手順 - -- 設定を `version: 1` のまま使う端末は影響を受けない。問題が出たら、この Pull Request の - マージを revert する。`version: 2` へ切り替えた端末は `devbase env backend use openbao - --layout flat` で従来の置き場へ戻す(サーバ上のデータは移さないため、移し直しの前なら従来の - パスに値が残っている) - -### 完了の定義 - -- [ ] 受け入れ条件 1〜18(5a を含む)をすべて満たし、テスト設計の各行に対応するテストがある -- [ ] `uv run pytest tests/`、`ruff check lib`、`python -m compileall -q lib bin` がいずれも exit=0