Skip to content

Commit 3e44d79

Browse files
takemi-ohamaclaude
andauthored
feat: PLAN35-runtime 起動ラッパーとコンテナへの受け渡し (#93)
* feat: 機密を平文ファイルを介さずコンテナへ渡す経路へ移行する 起動ラッパーが共通の機密ファイルを source するのをやめ、機密は Python 本体が 必要になった時点で復号してメモリ上で合成する。コンテナへは変数名だけを列挙 した構成で渡すため、暗号文も平文ファイルも Docker Compose には渡らない。 - 起動ラッパーは非機密設定 ($DEVBASE_ROOT/env) だけを読む。シェルから読める 場所に機密を置かないことが暗号化の前提であり、ここで読むと意味が無くなる - ホスト側で機密を必要とする処理は実測で 2 系統だけだった。Docker Compose の 変数展開 (ローカル S3 互換サービスの資格情報) と、それを含むビルド呼び出し。 ビルドは devbase env exec 経由にして Python 側から環境変数を渡す - 生成する構成へは変数名のみを書き、値は devbase 自身の環境変数から解決させる。 台数拡張時に生成する構成そのものへ書き込むため、別ファイルの上書きを重ねる 必要がなく適用順序の問題が起きない - 重ね順は従来の env_file の並びを維持する。共通機密とプロジェクト機密のキーを 列挙しつつ、両方に同じキーがある場合は値として非機密設定側を採用する。 environment は env_file より優先されるため、こうしないと「プロジェクト設定が 共通設定を上書きする」関係が反転する - 実在しない env_file 参照は生成時に落とす。暗号化で平文が無くなった参照が 残っていると Compose が起動時に落ちるため 移行コマンド (encrypt / decrypt) を追加した。暗号化は「読み戻せることを確認して から平文を退避する」順序で行う。鍵の指定を誤ったまま平文を失うと、誰にも復号 できないファイルだけが残るため。退避した平文は自動では消さず、場所を案内する。 構成ファイルの書き換えは行のコメントアウトで行い、元の行をそのまま残す。YAML と して読み書きし直すと利用者のコメントや整形が失われること、および平文へ戻す操作で 元の行を機械的に復元できることの 2 点による。 コンテナ起動前の設定チェックは、ファイルの有無ではなく秘密ストアに設定があるかで 判定する。移行済みの環境で毎回 env init が走るのを避けるため。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Z922jZ4R3488KR3GETgS1 * fix: env encrypt/decrypt が途中で失敗しても中間状態を残さないようにする encrypt / decrypt はどちらも対象ごとにループ内で破壊的な操作を実行していたため、 後続の対象で失敗すると先行対象だけ移行済みになり、しかも compose.yml の書き換えは 実行されず「構成ファイルが存在しないファイルを参照する」壊れた状態で終わっていた。 また _apply_compose_changes は書き込み失敗をログに出して次のファイルへ進むため、 機密の移動・削除が済んだあとでもコマンドが成功扱いになっていた。 実行した操作ごとに取り消し手続きを積み、どこで失敗しても逆順に巻き戻す _Rollback を 追加し、encrypt / decrypt の両方で共有する。 - encrypt: 全対象の暗号化と読み戻し検証 → 平文の退避 → compose.yml の書き換え - decrypt: 全対象の復号確認 (生バイト列も控える) → 平文の書き出し → compose.yml の復元 → 最後に暗号文を削除。破壊的な削除を最後に置くことで、 途中で失敗したときに失うものを最小にする - _apply_compose_changes は書き込み失敗を MigrationError で呼び出し元へ伝え、 書けたぶんは巻き戻す。巻き戻し自体が失敗したら何が残っているかを列挙する Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Z922jZ4R3488KR3GETgS1 * fix: 部分復号・空行・注入スキップ・インライン env_file の取りこぼしを直す PR #93 のレビュー指摘 4 件に対応する。 - compose_migrate.enable() に disable() と同じ targets 引数を持たせた。 `env decrypt --project` で一部だけ復号したとき、これまでは compose 内の 全マーカーを戻していたため、まだ暗号化されたままの共通設定 (${DEVBASE_ROOT}/.env) の参照まで有効になり Compose の起動が失敗していた。 無効化と復元で同じ判定 (_compose_targets) を使うようにして揃えた。 キー行 (`env_file:`) は有効なエントリが 1 つ以上戻ったときだけ復元する。 - disable() が env_file リスト内の空行で break していたのをスキップに変えた。 空行以降のエントリを無効化し損ねるうえ、「有効なエントリ 0 件」と誤判定して `env_file:` キー自体をコメントアウトし、起動失敗を招いていた。ブロックの 終端判定はインデントが受け持つため、空行で止める必要はない。 - 機密注入のスキップ判定を (コマンド, サブコマンド) の組に変えた。args.command にはトップレベルしか入らないため、鍵がまだ無い / 復号できない状態で実行される `env keygen` / `env encrypt` / `env decrypt` でも注入が走っていた。 - 行単位では扱えない env_file 記法 (インライン配列・単一文字列) を検出して ファイルと行番号つきで警告するようにした。移行の対象から漏れることを黙って いると、利用者は壊れた構成のまま起動して初めて気付く。対応範囲はモジュールの docstring にも明記した。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Z922jZ4R3488KR3GETgS1 * fix: scale 生成で非機密 environment を残し、機密以外の env_file 欠落を隠さない 生成する構成ファイル (.docker-compose.scale.yml) の作り方を 2 点直した。 - environment を丸ごと落としていたため、元の compose.yml が持つ非機密の固定値や 機能フラグまで消え、スケールした途端に生成コンテナの挙動が変わっていた。 secret_env_names に挙がったキーだけを値なし参照へ置き換え、それ以外は値ごと 残すようにした。元の記法は尊重し、map なら値 None の map、list なら裸のキー名 として出力する。機密キーの値が生成ファイルに残らないことは従来どおり保証する。 - 実在しない env_file 参照を無条件に落としていたため、利用者のタイプミスや未配置 の必須設定まで黙って成功扱いになり、Compose が知らせてくれる構成不備を隠して いた。落とす対象を「暗号化移行で消える既知の機密参照」に限定し、判定は compose_migrate.is_secret_entry (新規の公開関数) に集約した。 既存テストのうち、environment を落とす前提だったものは新仕様に合わせて更新した。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Z922jZ4R3488KR3GETgS1 * fix: 移行の中断条件を厳しくし、compose.yml の書き込みと機密の渡し先を直す 暗号化移行が「機密だけ退避されて構成は壊れたまま成功する」経路を塞ぎ、 生成する構成で非 dev サービスに機密が渡らない問題を直す。 - compose.yml を読めない場合は警告してスキップせず MigrationError で移行 全体を中止する。飛ばして続けると平文だけが退避され、存在しないファイルを 指す参照が残ったままコマンドが成功してしまうため - 機密ファイルを指すインライン記法 (`env_file: [.env]` / `env_file: .env`) を検出したら移行を失敗させ、手で直してからの再実行を案内する。機密と 無関係なインライン記法は移行に影響しないので従来どおり警告のみ。判定は compose_migrate.secret_inline_env_file_lines() に切り出した - compose.yml の書き込みを write_secure_bytes_atomic へ差し替え、途中で 失敗しても部分的なファイルが残らないようにする。compose.yml は機密では ないため、既存ファイルの権限を読み取って mode に渡し 0600 へ落とさない - 元々機密ファイルを env_file で参照していた非 dev サービス (db など) にも 機密の変数名を列挙する。移行後は参照がコメントアウトされ YAML から消える ため、compose.yml の生テキストを見る compose_migrate.services_with_secret_env_file() で渡し先を決める。参照を 持たないサービスには従来どおり注入しない - 末尾スペース / 行末コメント付きのエントリ (`- ${DEVBASE_ROOT}/.env # 共通設定`) が正しく無効化・復元されることを示すテストを追加 (現行実装で処理済み) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Z922jZ4R3488KR3GETgS1 * fix: 機密は「そのサービスが元々参照していた由来」のキーだけに絞って渡す これまでは「機密参照を持つか」の真偽だけで渡し先を決めていたため、共通の .env だけを参照していた db のようなサービスにも、プロジェクト専用のトークン まで全件が environment へ列挙されていた。元々受け取っていなかった機密が渡る のは機密範囲の拡大にあたるため、由来 (共通 / プロジェクト) 単位で絞り込む。 - compose_migrate.services_with_secret_env_file() の戻り値を 「サービス名 → 参照種別の集合 (TARGET_GLOBAL / TARGET_PROJECT)」へ変更。 コメントアウト済みの参照も従来どおり種別つきで数える - runtime.SecretEnv に global_names / project_names を持たせ、names は 両者を畳んだ全体を返すプロパティへ (呼び出し側の互換は維持) - generate_scaled_compose は非 dev サービスへ、そのサービスが参照していた 由来のキーだけを列挙する。dev は従来どおり全件 - 生テキストを読めない場合の「dev のみ・全件」フォールバックは維持 既知の限界として、同じキーが共通機密とプロジェクト機密の両方にある場合は Compose が実行プロセスの環境変数から 1 つの値しか解決できないため、共通側 だけを参照していたサービスにも合成後 (プロジェクト優先) の値が渡る。値を サービスごとに変えるには生成ファイルへ機密の値を書く必要があり、本 PR の 前提と矛盾するため受け入れる。コードコメントと plan35.md §7 に明記した。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Z922jZ4R3488KR3GETgS1 * fix: env_file ブロック内のコメント行で走査を止めない `env_file:` 配下に利用者が書いた単独のコメント行があると `_LIST_ITEM_RE` に一致せず走査が打ち切られ、コメント行より後ろの機密参照が 無効化されないまま残っていた。その状態で平文を退避すると、Compose が存在 しないファイルを参照して起動できなくなる。 空行と同じく単独のコメント行も読み飛ばすようにし、ブロックの終端は インデントだけが決めるようにした。無効化済みの行 (DISABLED_MARK 付き) は 見た目がコメント行でも中身はエントリなので、コメント判定より先に除いて いる (順序を誤ると enable が何も復元できなくなる)。 disable / enable で重複していたブロック走査は `_scan_env_file_block` へ 括り出した。片方だけ直すと無効化と復元がずれるため。 `services_with_secret_env_file` も同じ理由で参照種別を取りこぼしていたので、 共通の `_is_skippable` で読み飛ばすようにした。 テストは、既存の test_user_comments_are_preserved が「何も書き換えられて いない」ために往復の一致だけで通っていた点を補強し (修正前にこのアサートが失敗することを確認済み)、コメント行の後ろの参照が 無効化されること・往復で元に戻ること・コメントと空行が混在する場合・ services_with_secret_env_file が種別を拾えることを追加した。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Z922jZ4R3488KR3GETgS1 * fix: env_file の long syntax・クォート付きサービス名・CRLF を取りこぼさない 行ベースの走査に残っていた 3 つの穴を塞ぎ、あわせて「何を扱い、何を扱わないか」 をモジュールの契約として docstring に書き出した。 - long syntax (`- path: .env`) を参照として認識する。1 行で閉じているものは 従来どおり無効化・復元し、`required: false` などの続きの行を持つ形・フロー 記法・シーケンスでない値は書き換えず、機密を指していれば移行を中止する。 続きの行で走査を打ち切らないので、その後ろに並ぶ機密参照も取りこぼさない。 - サービス名を YAML と同じ姿へ正規化する。`"db":` を引用符込みで記録すると パース済みの `db` と一致せず、そのサービスへ機密が渡らなかった。 - 各行の元の行末を保って書き換える。`rstrip('\n') + '\n'` で CRLF が LF に 変わり、encrypt → decrypt の往復で元の compose.yml に戻らなかった。移行 コマンド側も read_bytes で読み、改行コードを勝手に揃えないようにした。 インライン記法だけを対象にしていた中止判定は扱えない記法全体に広げ、名前を secret_unsupported_env_file_lines へ変更した。扱えない記法は黙って通さず、 必ず中止か警告のどちらかに落ちる不変条件を docstring に明記している。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Z922jZ4R3488KR3GETgS1 * fix: 単一文字列の env_file を中止せず移行対象に含める `env_file: .env` のように値が単一文字列で書かれた定義は、これまで「行単位では 扱えない記法」として扱い、機密を指す場合は移行ごと中止していた。利用者は手で `- ...` の並びへ書き換えないと暗号化できず、実質的に使えない状態だった。 この形はエントリが 1 つしかなく 1 行で完結するため、`env_file:` の行そのものを コメントアウトすれば安全に無効化でき、`enable` でも元のバイト列へ戻せる。 `_inline_scalar_ref` で「1 行で完結する単一文字列」だけを切り出し、disable / enable / 中止判定 (`secret_unsupported_env_file_lines`) の 3 箇所で同じ判定を 使うようにした。 フロー記法 (`env_file: [ ... ]` / `{ path: ... }`)、ブロックスカラー、閉じていない クォートなど 1 行で安全に判断できない記法は、従来どおり警告・中止のままにする。 機密を指さない単一文字列 (`env_file: config/app.env`) は触らない。モジュール 冒頭の「扱う記法 / 中止する記法 / 触らない記法」の契約も実装に合わせて更新した。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Z922jZ4R3488KR3GETgS1 * fix: 書き換え後の compose.yml を YAML として検証し機密参照の残りを検出する `env_file: >-` のようなブロックスカラーは先頭行に参照先が無く、行ベースの 走査では機密を指しているか判別できない。その結果 `env_file: >-` +次行 `.env` の構成は無効化も中止もされず、暗号化後も存在しない平文への参照が残ったまま コマンドが成功していた。モジュールが掲げる「扱えない記法は必ず中止か警告に 落ちる」という不変条件が破れている。 記法ごとに穴を塞ぐ対応では同種の見落としが出続けるため、記法の判別に依らない 事後検証を最後の砦として追加する: - `compose_migrate.remaining_secret_env_file_refs()` を追加。書き換え後の テキストを `yaml.safe_load` でパースし、各サービスの `env_file` に残った 機密参照を返す。文字列 / 文字列のリスト / long syntax の dict のいずれも 平坦化して拾う。無効化した行は YAML のコメントなのでパーサからは見えず、 残っていれば走査が取りこぼしたことを意味する - パースできない `compose.yml` は `ComposeParseError` を投げる。検証できない 以上「参照が無い」とは言い切れないため、読み取り失敗と同じ扱いで中止する - `env_migrate` の暗号化側で全 `compose.yml` に検証を掛け、残っていれば どのファイルのどのサービスにどの参照が残るかを示して `MigrationError` で 中止する。差分ゼロのファイルも対象にする (走査が何も見つけられなかった ファイルこそ取りこぼしの疑いが濃い) - 復号側では行わない。平文が戻る以上その参照は有効で正しく、ここで止めると 壊れた状態からの復帰手段を塞いでしまう 行ベースの走査は「うまく書き換えられれば書き換える、取りこぼしたら事後検証が 止める」という二段構えになる。既存の `secret_unsupported_env_file_lines()` はより早い段階で分かりやすいエラーを 出すための仕組みとして残す。設計意図はモジュール docstring とコード内 コメントに明記した。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Z922jZ4R3488KR3GETgS1 * fix: 退避先を排他的に作り、機密は原文のバイト列のまま往復させる - バックアップ先の衝突で過去の平文を失わないようにする 秒単位の日時ディレクトリは既存のバックアップと衝突しうる。従来は `exist_ok=True` で掘っていたため、衝突すると `shutil.move` が同名の `global.env` / プロジェクトの env を上書きし、「削除しないはずの過去の 平文」を失っていた。`_create_backup_dir` を追加して `exist_ok=False` で 排他的に作成し、既にあれば `-2` `-3` … と一意な名前へ寄せる。上限 (100 回) まで空きが無ければ平文に触れないまま中止する。退避先には平文の 機密が置かれるため 0700 で作る (親は他機能と共有するので既定のまま)。 - 往復でコメント・空行・`export` 表記が失われないようにする 暗号化時に平文を辞書へ畳んでいたため、`decrypt` してもコメント・空行・ `export KEY=...` 表記・値のクォートが戻らず、案内している「暗号化前の 状態へそのまま復帰」を満たしていなかった。`SecretStore` / `PlaintextBackend` / `AgeBackend` に生バイト列を扱う `save_bytes` / `load_bytes` を追加し、移行は原文のバイト列のまま暗号化・復元する。 読み戻し検証も「暗号化 → 復号 → 元のバイト列と一致」で維持する。 辞書経由の `save` / `load` はそのまま残し、`env set` などで値を書き換えた ときに正規化されるのは平文だけを使っていた頃と同じ挙動として変えない (「値を書き換えるまでは原文が保たれ、書き換えると正規化される」)。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Z922jZ4R3488KR3GETgS1 * fix: プロジェクト切替で切替元の機密が環境変数に残らないようにする cli._load_secret_env は dispatch の前に「現在地のプロジェクト」の機密を os.environ へ載せるが、TUI や `project up <other>` の直接起動ではその後に 対象プロジェクトへ切り替わる。切替先に同名キーが無い機密 (切替元固有の トークン等) は載せ直しでは上書きされず残り、Compose や子プロセスへ 引き継がれてしまう。 - runtime.inject が「載せた変数名とその注入前の値」を記録し、 runtime.clear_injected で注入前の状態へ戻せるようにした。自分が載せた キーだけを対象にし、利用者がシェルで設定していた同名の変数は元の値へ 戻すので消えない - container._inject_secrets は対象プロジェクトへ chdir した後に呼ばれる ため、載せ直しの前に clear_injected を通して切替元の機密を落とす - _resolve_project_name は os.chdir と併せて PWD も切り替える。機密の 解決 (runtime.current_project_name) は wrapper の cd を前提に PWD を 先に見るため、PWD が切替前のままだと切替先ではなく呼び出し元の機密を 読んでしまい、載せ直しが機能しないため (TUI の _run_in_project と同様) 非機密設定 (env) 側の _CALLER_ENV_KEYS / _resolve_project_name と同じ性質を 機密にも与えることになる。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Z922jZ4R3488KR3GETgS1 * fix: 注入履歴を対象の環境マッピングごとに持たせる 注入履歴がモジュールレベルに 1 つしか無かったため、inject(environ=A) の後に clear_injected(environ=B) を呼ぶと、A に対して記録した内容で B を誤って 「復元」し、かつ A には機密が載ったまま残っていた。 履歴を「どの環境マッピングへ注入したか」と結び付け、clear_injected は同じ 対象に記録された履歴だけを解除して、その対象の履歴を破棄するようにした。 dict は hashable でないため id() をキーにするが、対象そのものへの参照も 一緒に保持し、id の再利用による誤爆を防ぐ。os.environ を既定対象とする 従来の使い勝手は変えていない。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Z922jZ4R3488KR3GETgS1 --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 26dae89 commit 3e44d79

22 files changed

Lines changed: 5514 additions & 96 deletions

‎bin/devbase‎

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,18 @@ env_var_keys() {
3636
# Environment setup
3737
export DOCKER_GID=$( [ "$(uname)" = "Darwin" ] && echo "0" || grep docker /etc/group | cut -d: -f3 )
3838
export COMPOSE_PROJECT_NAME=$(basename "$PWD")
39-
# devbase root の .env (AWS / BigQuery 等の devbase ツール用変数) を読み込む。
39+
# devbase root の非機密設定 (env) を読み込む。
40+
#
41+
# 機密 (認証情報) はここでは読まない。暗号化された機密は Python 本体が必要に
42+
# なった時点で復号し、値を必要とする処理へ環境変数として渡す (plan35 §4.4)。
43+
# シェルから読める場所に機密を置かないことが暗号化の前提なので、ここで
44+
# `${DEVBASE_ROOT}/.env` を source すると意味が無くなる。
45+
#
4046
# project ディレクトリで実行された場合に Laravel ランタイム用 .env を bash で
4147
# source すると、CRLF 改行や `|` / `&` 等の特殊文字を含む値で syntax error に
4248
# なる。compose は同階層の .env を自動で読むため wrapper 側で project .env を
4349
# source する必要は無い。
44-
[ -f "${DEVBASE_ROOT}/.env" ] && set -a && source "${DEVBASE_ROOT}/.env" && set +a
50+
[ -f "${DEVBASE_ROOT}/env" ] && set -a && source "${DEVBASE_ROOT}/env" && set +a
4551

4652
# 呼び出し元 (初期 CWD) の env で定義された変数キーを記録しておく。
4753
# project 切替 (maybe_cd_project) 時に「呼び出し元プロジェクトにしか無い変数」を
@@ -61,6 +67,15 @@ export DEVBASE_ROOT
6167
# Function definitions
6268
# ===================================================================
6369

70+
# Docker Compose の変数展開が機密を必要とする場合があるため、compose の呼び出しは
71+
# Python 経由で機密を注入して実行する (plan35 §4.4 / §11.2)。復号結果は子プロセスの
72+
# 環境変数としてだけ渡り、ファイルには書き出されない。
73+
compose_with_secrets() {
74+
ensure_uv
75+
PYTHONPATH="${DEVBASE_ROOT}/lib:$PYTHONPATH" \
76+
uv run --project "$DEVBASE_ROOT" python -m devbase.cli env exec -- "$@"
77+
}
78+
6479
cmd_build() {
6580
echo "=== Building devbase images ==="
6681

@@ -154,7 +169,7 @@ cmd_build() {
154169

155170
echo ""
156171
echo "[2/2] Building project image without cache..."
157-
if docker compose build "${DEV_SERVICE_NAME:-dev}" --no-cache "$@"; then
172+
if compose_with_secrets docker compose build "${DEV_SERVICE_NAME:-dev}" --no-cache "$@"; then
158173
echo ""
159174
echo "✓ All images built successfully"
160175
else
@@ -176,7 +191,7 @@ cmd_build() {
176191

177192
echo ""
178193
echo "[2/2] Building project image..."
179-
if docker compose build "${DEV_SERVICE_NAME:-dev}" "$@"; then
194+
if compose_with_secrets docker compose build "${DEV_SERVICE_NAME:-dev}" "$@"; then
180195
echo ""
181196
echo "✓ All images built successfully"
182197
else
@@ -209,7 +224,7 @@ cmd_build() {
209224

210225
echo ""
211226
echo "[2/2] Building project image..."
212-
if docker compose build "${DEV_SERVICE_NAME:-dev}" "$@"; then
227+
if compose_with_secrets docker compose build "${DEV_SERVICE_NAME:-dev}" "$@"; then
213228
echo ""
214229
echo "✓ All images built successfully"
215230
else

‎docs/user/cli-reference/03-env.md‎

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,78 @@ devbase env keygen --force
152152

153153
> **鍵のバックアップは必須です。** この鍵を失うと、暗号化した機密は誰にも復号できません(devbase 側にも復旧手段はありません)。生成後に表示される鍵ファイルを、パスワード管理ツールなど端末とは別の場所へ必ず複製してください。鍵は全ワークスペース共通のため、`--force` で作り直すと他のワークスペースで暗号化した機密も復号できなくなります。
154154
155+
## `devbase env encrypt`
156+
157+
平文で保存されている設定を、暗号化ストア (`$DEVBASE_ROOT/secrets/`) へ移します。事前に `devbase env keygen` で鍵を作っておく必要があります。
158+
159+
```
160+
devbase env encrypt [--project NAME]... [--dry-run] [-y|--yes]
161+
```
162+
163+
| オプション | 説明 |
164+
|-----------|------|
165+
| `--project NAME` | 対象を指定プロジェクトだけに絞る(繰り返し指定可)。指定すると共通設定は対象外になります |
166+
| `--dry-run` | 変更内容と構成ファイルの差分を表示するだけで、何も書き換えません |
167+
| `-y`, `--yes` | 確認プロンプトを省略 |
168+
169+
実行すると次の 3 つが行われます。
170+
171+
1. 平文の設定を暗号化して `secrets/` 配下へ保存する
172+
2. **暗号化した内容を読み戻して元と一致することを確認**してから、元の平文を `backups/env-encrypt/<日時>/` へ退避する
173+
3. 各プロジェクトの `compose.yml` から機密ファイルの参照をコメントアウトする(元の行はコメントとして残るため、`decrypt` で復元できます)
174+
175+
```bash
176+
# 何が変わるかを先に確認する
177+
devbase env encrypt --dry-run
178+
179+
# 共通設定とすべてのプロジェクトを暗号化する
180+
devbase env encrypt
181+
182+
# 特定プロジェクトだけを暗号化する
183+
devbase env encrypt --project web
184+
```
185+
186+
> 退避した平文は**自動では消しません**。内容を確認したうえで、案内された `backups/env-encrypt/<日時>/` を削除してください。削除するまでは端末上に平文の認証情報が残ったままです。
187+
188+
> 退避先は毎回新しく作られます。同じ秒に再実行して名前が衝突した場合は `<日時>-2`, `<日時>-3` … と別のディレクトリになり、**過去の退避物を上書きすることはありません**。
189+
190+
## `devbase env decrypt`
191+
192+
暗号化された設定を平文へ戻します。`encrypt` と対になる退避コマンドです。
193+
194+
```
195+
devbase env decrypt [--project NAME]... [--dry-run] [-y|--yes]
196+
```
197+
198+
オプションは `encrypt` と同じです。`compose.yml` のコメントアウトも元に戻るため、暗号化前の状態へそのまま復帰します。機密ファイルは `KEY=VALUE` の一覧へ畳まず原文のバイト列のまま暗号化しているので、コメント・空行・`export KEY=...` 表記・値のクォートもそのまま戻ります。
199+
200+
> 原文が保たれるのは**値を書き換えるまで**です。暗号化した状態で `devbase env set` などを実行すると、内容は `KEY=VALUE` を昇順に並べた書式へ正規化され、コメントは残りません(平文だけを使っていた頃と同じ挙動です)。
201+
202+
```bash
203+
devbase env decrypt --dry-run
204+
devbase env decrypt
205+
```
206+
207+
## `devbase env exec`
208+
209+
復号した機密を環境変数として渡した状態で、任意のコマンドを実行します。値はその子プロセスの環境変数としてのみ渡り、ファイルには書き出されません。
210+
211+
```
212+
devbase env exec -- CMD [ARGS...]
213+
```
214+
215+
起動ラッパーは共通の機密ファイルを読み込まないため、ホスト側で機密を必要とする処理(Docker Compose の変数展開など)はこのコマンドを通します。devbase 自身の `devbase build` も内部でこれを使っています。
216+
217+
```bash
218+
# コンテナに渡る値を確認する
219+
devbase env exec -- printenv ANTHROPIC_API_KEY
220+
221+
# 機密を必要とする compose 操作を手で実行する
222+
devbase env exec -- docker compose config
223+
```
224+
225+
> `devbase env exec -- printenv` のように値を表示するコマンドは、画面共有や端末ログに認証情報がそのまま残ります。実行する場面に注意してください。
226+
155227
## `devbase env export`
156228

157229
複数プロジェクトの `.env` 群を暗号化したまま 1 つのバンドルにまとめて書き出します。

‎docs/user/cli-reference/README.md‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ devbase の全コマンドの構文、オプション、使用例をまとめた
66
|---------|------|
77
| [トップレベルコマンド](01-toplevel.md) | `init` / `status` / `bin/rc` |
88
| [project グループ](02-project.md) | コンテナのライフサイクル管理・一覧(`up` / `down` / `login` / `ps` / `logs` / `scale` / `build` / `rebuild` / `list`)と非推奨の `container` グループ |
9-
| [env グループ](03-env.md) | 環境変数の管理(`init` / `sync` / `list` / `set` / `get` / `delete` / `edit` / `project` / `keygen` / `export` / `import`) |
9+
| [env グループ](03-env.md) | 環境変数の管理(`init` / `sync` / `list` / `set` / `get` / `delete` / `edit` / `project` / `keygen` / `encrypt` / `decrypt` / `exec` / `export` / `import`) |
1010
| [plugin グループ](04-plugin.md) | プラグインの管理(`list` / `install` / `uninstall` / `update` / `info` / `sync` / `migrate` / `repo *`) |
1111
| [snapshot グループ](05-snapshot.md) | スナップショットの管理(`create` / `list` / `restore` / `copy` / `delete` / `rotate`) |
1212

@@ -26,7 +26,9 @@ graph TD
2626
D --> D3["login [index]"]
2727
D --> D4["build [image] / rebuild [name]"]
2828
D --> D2["list [--no-interactive]"]
29-
E --> E1[init / sync / list / set / get / delete / edit / project / export / import]
29+
E --> E1[init / sync / list / set / get / delete / edit / project]
30+
E --> E2[keygen / encrypt / decrypt / exec]
31+
E --> E3[export / import]
3032
F --> F1[list / install / uninstall / update / info / sync / migrate]
3133
F --> F2[repo add / repo remove / repo list / repo refresh]
3234
G --> G1[create / list / restore / copy / delete / rotate]

‎etc/_devbase‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,9 @@ _devbase() {
106106
'export:Export .env files as an encrypted bundle (age)'
107107
'import:Import .env bundle (age decrypt + merge)'
108108
'keygen:Generate the devbase age key used by the secret store'
109+
'exec:Run a command with the decrypted secrets in its environment'
110+
'encrypt:Move plaintext settings into the encrypted store'
111+
'decrypt:Move encrypted settings back to plaintext'
109112
)
110113

111114
plugin_subcommands=(
@@ -288,6 +291,15 @@ _devbase() {
288291
'--backup-dir[Override backup directory]:dir:_files -/' \
289292
'--keep-last[Keep only the last N backup directories]:n:'
290293
;;
294+
exec)
295+
_arguments '*:command:_command_names -e'
296+
;;
297+
encrypt|decrypt)
298+
_arguments \
299+
'*--project[Limit to the specified project (repeatable)]:name:' \
300+
'--dry-run[Show what would change without writing]' \
301+
'--yes[Skip the confirmation prompt]' '-y[Skip the confirmation prompt]'
302+
;;
291303
keygen)
292304
_arguments \
293305
'--force[Overwrite an existing key]' \

‎etc/devbase-completion.bash‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ _devbase_completions() {
3535
# project / container は同じサブコマンド群 (container は非推奨だが補完は維持)。
3636
local project_subcommands="up down ps login logs scale build rebuild list"
3737
local container_subcommands="up down ps login logs scale build rebuild"
38-
local env_subcommands="init sync list set get delete edit project export import keygen"
38+
local env_subcommands="init sync list set get delete edit project export import keygen exec encrypt decrypt"
3939
local plugin_subcommands="list install uninstall update info sync repo"
4040
local repo_subcommands="add remove list refresh"
4141
local snapshot_subcommands="create list restore copy delete rotate"

‎issues/plan35.md‎

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@ chmod 600 ~/.config/devbase/age/keys.txt
230230
- **コンテナ環境の可視性**: コンテナの詳細情報を参照する権限があれば、注入済みの環境変数は読める。devbase は開発コンテナに Docker の制御ソケットを渡す構成を既定に含むため、**コンテナ内から他コンテナの環境変数も参照できる**
231231
- **構成の展開結果**: 構成の確認コマンドは変数名だけの列挙を実際の値へ解決して表示する
232232
- **利用者権限を得た攻撃者**: 既定の鍵保管では鍵も同時に読める
233+
- **同名キーの由来分離**: 生成する構成はサービスごとに「元々参照していた由来(共通/プロジェクト)のキーだけ」を列挙するが、同じキーが共通機密とプロジェクト機密の両方にある場合、値は devbase 自身の環境変数から解決されるため 1 つ(プロジェクト側が優先)に定まる。結果として共通側だけを参照していたサービスにも合成後の値が渡る。サービスごとに異なる値を渡すには生成ファイルへ値を書き込むしかなく、「生成物に機密の値を残さない」という本方針の前提と矛盾するため受け入れる
233234

234235
実行時の露出を下げる手段(コンテナの秘密情報機能によるファイル渡し、クラウドの一時認証、認証エージェントの転送)は、本方針の範囲外として将来の課題に置く。
235236

@@ -255,9 +256,18 @@ chmod 600 ~/.config/devbase/age/keys.txt
255256

256257
## 10. 未確認事項・残リスク
257258

258-
- **値を持たない変数名の列挙の挙動**: 実行プロセス側で変数が未設定だった場合に、Docker Compose の版によって警告のみか失敗かが分かれる可能性がある。実装前に対象版で確認する
259-
- **ホスト側処理の機密依存範囲**: 起動ラッパーが読み込んだ環境変数に依存する処理を全件は洗い出せていない。段階 3 の着手時に、機密を必要とする処理の一覧化を先に行う
260-
- **コンテナ台数を増やした構成での上書き順序**: 台数拡張時に生成される構成ファイルと、機密を渡す上書き構成の適用順序は未検証
259+
- ~~**値を持たない変数名の列挙の挙動**~~: 確認済み。Docker Compose v5.1.4 では、実行プロセス側で未設定の変数は失敗ではなく空 (`null`) として扱われ、その変数はコンテナへ渡らない。
260+
261+
```console
262+
$ DEFINED_VAR=hello docker compose config
263+
environment:
264+
DEFINED_VAR: hello
265+
UNDEFINED_VAR: null
266+
```
267+
268+
同時に、構成の確認コマンドが設定済みの値をそのまま表示することも確認できた (§7「守れないもの」に挙げた挙動)。
269+
- ~~**ホスト側処理の機密依存範囲**~~: 洗い出し済み。§11.2 を参照
270+
- ~~**コンテナ台数を増やした構成での上書き順序**~~: 別ファイルの上書きを重ねる方式をやめ、台数拡張時に生成する構成そのものへ変数名の列挙を書き込む方式にした。適用順序の問題自体が発生しない
261271
- **復号の実行回数**: 現在は devbase の実行ごとに設定ファイルを読み込んでいる。復号を毎回行う場合の所要時間は未計測であり、体感が悪ければ実行単位での保持を検討する
262272
- **バックアップ機能との関係**: バックアップ取得がボリューム内の機密を平文で保存するかは未確認
263273

0 commit comments

Comments
 (0)