Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
__pycache__/
.venv/
.env
.env.bak
.env.backup
# 日時付きの控え (.env.bak-20260807172231 等) は完全一致では弾けない。
# 実際に未追跡のまま検出された経緯があるためワイルドカードで除外する。
.env.bak*
.env.backup*
.gemini/
.docker-compose.scale.yml
plugins.yml
Expand Down
1 change: 0 additions & 1 deletion containers/lfm/compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,3 @@ services:
build:
context: .
dockerfile: Dockerfile
c
3 changes: 3 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ graph TD
| [CLI リファレンス](user/cli-reference/README.md) | 全コマンドの構文・オプション・使用例 |
| [プラグインレジストリ](user/plugin-registries.md) | 公開・社内レジストリの一覧と追加方法 |
| [環境変数ガイド](user/environment-variables.md) | 3レベル構造、コレクター、ソース同期 |
| [環境変数の暗号化](user/env-encryption.md) | 認証情報を暗号化して保存する / 鍵とチーム共有 / 平文へ戻す |
| [コンテナ操作ガイド](user/container-operations.md) | ライフサイクル、並行開発、ボリューム構造 |
| [スナップショットガイド](user/snapshot-guide.md) | 増分バックアップ、世代管理、復元手順 |
| [トラブルシューティング](user/troubleshooting.md) | カテゴリ別の問題と解決策 |
Expand Down Expand Up @@ -101,6 +102,7 @@ docs/
│ │ └── 05-snapshot.md ← snapshot グループ
│ ├── plugin-registries.md ← プラグインレジストリ
│ ├── environment-variables.md ← 環境変数ガイド
│ ├── env-encryption.md ← 環境変数の暗号化
│ ├── container-operations.md ← コンテナ操作ガイド
│ ├── snapshot-guide.md ← スナップショットガイド
│ └── troubleshooting.md ← トラブルシューティング
Expand All @@ -124,6 +126,7 @@ docs/
| devbase を初めてインストールする | [はじめに](user/getting-started.md#セットアップ手順) |
| コマンドの使い方を調べる | [CLI リファレンス](user/cli-reference/README.md) |
| 環境変数を設定する | [環境変数ガイド](user/environment-variables.md#環境変数の操作) |
| 認証情報を暗号化して保存する | [環境変数の暗号化](user/env-encryption.md) |
| 複数コンテナで並行開発する | [コンテナ操作ガイド](user/container-operations.md#並行開発) |
| データをバックアップ・復元する | [スナップショットガイド](user/snapshot-guide.md) |
| エラーが発生した | [トラブルシューティング](user/troubleshooting.md) |
Expand Down
55 changes: 55 additions & 0 deletions docs/user/cli-reference/03-env.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,61 @@ devbase env exec -- docker compose config

> `devbase env exec -- printenv` のように値を表示するコマンドは、画面共有や端末ログに認証情報がそのまま残ります。実行する場面に注意してください。

## `devbase env rekey`

誰が機密を復号できるかを変更し、暗号化済みの機密をまとめて暗号化し直します。

```
devbase env rekey [--add-recipient KEY]... [--remove-recipient KEY]... [--dry-run] [-y|--yes]
```

| オプション | 説明 |
|-----------|------|
| `--add-recipient KEY` | 受信者を追加(繰り返し指定可)。`age1...` / `ssh-ed25519 ...` / `@PATH` |
| `--remove-recipient KEY` | 受信者を削除(繰り返し指定可) |
| `--dry-run` | 変更内容を表示するだけで、何も書き換えません |
| `-y`, `--yes` | 確認プロンプトを省略 |

受信者は `$DEVBASE_ROOT/secrets/recipients.txt` に記録されます。リストがまだ無い状態で追加すると、自分の公開鍵も一緒に登録されます(登録しないと自分が受信者から外れ、自分の機密を復号できなくなるため)。

```bash
# 同僚を追加する
devbase env rekey --add-recipient age1xxxxxxxx...

# 抜けた人を外す
devbase env rekey --remove-recipient age1xxxxxxxx...
```

> 自分の公開鍵を受信者から外すと、再暗号化後にその端末では機密を復号できなくなります。実行前に警告が表示されます。

受信者リストの更新と全機密の再暗号化は、途中で失敗しても中途半端な状態を残さない 1 つのまとまりとして適用されます。書き込みに失敗した場合は受信者リストも各暗号文も実行前の内容へ戻るため、旧受信者宛と新受信者宛の暗号文が混在することはありません。

## `devbase env doctor`

端末上に残る平文と、除外設定の穴を点検します。問題が見つかると非ゼロで終了するため、定期実行にも使えます。

```
devbase env doctor
```

確認する内容:

| 観点 | 内容 |
|-----|------|
| 鍵 | 鍵ファイルの有無と権限、置き場のディレクトリ権限 |
| 保存先の衝突 | 暗号化ファイルと平文が同時に存在していないか |
| 退避された平文 | `backups/env-encrypt/` / `backups/env-import/` に平文が残っていないか |
| 控えファイル | `.env.bak-<日時>` のような平文の控えが残っていないか |
| 除外設定 | `.env` / `secrets/` 配下 / `.env.bak-<日時>` / `projects/<name>/.env` が実際に Git から除外されるか |

除外設定の点検は `.gitignore` を読んで解釈するのではなく、代表的なパスを `git check-ignore` に渡して **Git 自身に判定させます**(`.gitignore` の解釈は Git の実装が正であり、独自に真似ると書き方によって食い違うため)。ルート指定(`/.env`)でも任意階層(`**/.env`)でも、Git が実際に除外できていれば報告されません。逆に次のように **Git は除外しない** 書き方は、除外されないパスを挙げて報告します。

- `.env # 機密` — Git は行頭の `#` だけをコメントとして扱うため、これは `.env # 機密` というパターンになります
- `.env` の後に `!.env` — 後段の再包含で除外が取り消されます
- `secrets/*.age` — 配下の平文(`secrets/leftover.env` など)が漏れます

`DEVBASE_ROOT` が Git リポジトリでない場合や `git` が使えない場合は、「除外設定を確認できませんでした」と報告します(誤って「問題なし」とは言いません)。

## `devbase env export`

複数プロジェクトの `.env` 群を暗号化したまま 1 つのバンドルにまとめて書き出します。
Expand Down
4 changes: 2 additions & 2 deletions docs/user/cli-reference/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ devbase の全コマンドの構文、オプション、使用例をまとめた
|---------|------|
| [トップレベルコマンド](01-toplevel.md) | `init` / `status` / `bin/rc` |
| [project グループ](02-project.md) | コンテナのライフサイクル管理・一覧(`up` / `down` / `login` / `ps` / `logs` / `scale` / `build` / `rebuild` / `list`)と非推奨の `container` グループ |
| [env グループ](03-env.md) | 環境変数の管理(`init` / `sync` / `list` / `set` / `get` / `delete` / `edit` / `project` / `keygen` / `encrypt` / `decrypt` / `exec` / `export` / `import`) |
| [env グループ](03-env.md) | 環境変数の管理(`init` / `sync` / `list` / `set` / `get` / `delete` / `edit` / `project` / `keygen` / `encrypt` / `decrypt` / `exec` / `rekey` / `doctor` / `export` / `import`) |
| [plugin グループ](04-plugin.md) | プラグインの管理(`list` / `install` / `uninstall` / `update` / `info` / `sync` / `migrate` / `repo *`) |
| [snapshot グループ](05-snapshot.md) | スナップショットの管理(`create` / `list` / `restore` / `copy` / `delete` / `rotate`) |

Expand All @@ -27,7 +27,7 @@ graph TD
D --> D4["build [image] / rebuild [name]"]
D --> D2["list [--no-interactive]"]
E --> E1[init / sync / list / set / get / delete / edit / project]
E --> E2[keygen / encrypt / decrypt / exec]
E --> E2[keygen / encrypt / decrypt / exec / rekey / doctor]
E --> E3[export / import]
F --> F1[list / install / uninstall / update / info / sync / migrate]
F --> F2[repo add / repo remove / repo list / repo refresh]
Expand Down
144 changes: 144 additions & 0 deletions docs/user/env-encryption.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# 環境変数の暗号化

devbase が扱う認証情報(クラウドのアクセスキー、コード管理サービスの個人アクセストークン、各種 AI サービスの API キーなど)を、保存時に暗号化して持つためのガイドです。

暗号化しない運用も引き続き可能です。移行は明示的なコマンドで行い、いつでも平文へ戻せます。

## 何が変わるのか

| | 暗号化しない場合(既定) | 暗号化した場合 |
|---|---|---|
| 共通の機密 | `$DEVBASE_ROOT/.env` | `$DEVBASE_ROOT/secrets/global.env.age` |
| プロジェクトの機密 | `projects/<name>/.env` | `secrets/projects/<name>.env.age` |
| コンテナへの渡り方 | 構成ファイルが平文を直接読む | devbase が復号し、変数名だけを列挙した構成で渡す |
| 日々の操作 | `devbase env set` / `get` / `edit` … | **変わらない** |

保存先はファイルの存在から自動で判定されます。暗号化ファイルがあればそれを、無ければ平文を使います。

## 使いはじめる

### 1. 鍵を作る

```bash
devbase env keygen
```

鍵は `~/.config/devbase/age/keys.txt` に `0600` で作られます。場所を変えたい場合は `DEVBASE_AGE_KEY_FILE` を設定してから実行してください。

> **鍵のバックアップは必須です。** この鍵を失うと、暗号化した機密は誰にも復号できません(devbase 側にも復旧手段はありません)。パスワード管理ツールなど、端末とは別の場所へ必ず複製してください。

### 2. 何が変わるか確認する

```bash
devbase env encrypt --dry-run
```

暗号化される設定の一覧と、各プロジェクトの `compose.yml` に加わる変更の差分が表示されます。

### 3. 暗号化する

```bash
devbase env encrypt
```

次の順で処理されます。

1. 平文の設定を暗号化して `secrets/` 配下へ保存する
2. **暗号化した内容を読み戻して元と一致することを確認**する
3. 元の平文を `backups/env-encrypt/<日時>/` へ退避する
4. 各プロジェクトの `compose.yml` から機密ファイルの参照をコメントアウトする

途中で失敗した場合は、それまでの変更をすべて巻き戻して中止します。「一部だけ暗号化され、構成は存在しないファイルを参照したまま」という状態にはなりません。

### 4. 退避された平文を消す

`encrypt` は元の平文を**自動では削除しません**。内容を確認してから、案内されたディレクトリを削除してください。

```bash
rm -rf $DEVBASE_ROOT/backups/env-encrypt/<日時>
```

消し忘れは `devbase env doctor` が指摘し続けます。

## 日々の操作

暗号化しても操作は変わりません。

```bash
devbase env list # 暗号化された保存先には [暗号化] と表示される
devbase env set KEY=VALUE
devbase env get KEY
devbase env delete KEY --project
devbase env edit # 復号 → 編集 → 再暗号化
```

`devbase env edit` は、復号結果を自分専用の一時ディレクトリへ `0600` で書き、編集後に暗号化し直してから必ず削除します。エディタへ値を渡す手段が他に無いため、**この操作の間だけ平文が一瞬ディスクに載ります**。

## 点検する

```bash
devbase env doctor
```

以下を確認し、問題があれば非ゼロで終了します。

- 鍵ファイルとその置き場の権限
- 暗号化ファイルと平文が同時に存在していないか
- 移行時・取り込み時に退避された平文が残っていないか
- 日時付きの控えファイル(`.env.bak-20260807172231` など)が残っていないか
- `.env` / `secrets/` 配下 / `.env.bak-<日時>` / `projects/<name>/.env` が実際に Git から除外されるか(`git check-ignore` で Git 自身に判定させます。`DEVBASE_ROOT` が Git リポジトリでなければ「確認できませんでした」と報告します)

## チームで共有する

各メンバーの公開鍵を受信者として登録すると、秘密鍵を渡さずに同じファイルを共同で使えます。

```bash
# 同僚の公開鍵を追加して、既存の機密をまとめて暗号化し直す
devbase env rekey --add-recipient age1xxxxxxxx...

# 抜けた人を外す
devbase env rekey --remove-recipient age1xxxxxxxx...

# 何が変わるか先に見る
devbase env rekey --add-recipient age1xxxxxxxx... --dry-run
```

受信者は `$DEVBASE_ROOT/secrets/recipients.txt` に記録されます。中身は公開鍵だけですが、第三者が自分の鍵を追記できると以後の暗号化がその相手にも復号可能になるため、`0600` で保護されます。

自分の公開鍵を受信者から外そうとすると警告が出ます。外したまま再暗号化すると、その端末では機密を復号できなくなります。

## 持ち運ぶ

`devbase env export` / `import` はそのまま使えます。書き出しは暗号化された機密を復号してバンドルへ入れ、バンドル自体を age で暗号化します。取り込み先が暗号化されていれば、**取り込み結果も暗号化されたまま**保存されます(平文の `.env` は作られません)。

```bash
devbase env export bundle.dbenv --recipient age1xxxxxxxx...
devbase env import bundle.dbenv --identity ~/.config/devbase/age/keys.txt
```

## 平文へ戻す

```bash
devbase env decrypt
```

`compose.yml` のコメントアウトも元に戻り、暗号化前の状態へそのまま復帰します。コメントや空行、`export KEY=value` 表記も失われません(`devbase env set` などで値を書き換えた場合は、平文だけで運用していたときと同じく整形されます)。

## 守れること / 守れないこと

**守れること**: 端末のディスク上に残る保存ファイル、バックアップ、クラウド同期フォルダ、ファイル転送中、リポジトリへの誤コミット、画面共有時の誤表示。

**守れないこと**:

- **実行時の平文化**: 最終的にコンテナへは環境変数として平文で渡ります
- **コンテナ環境の可視性**: コンテナの詳細情報を参照できれば、注入済みの環境変数は読めます。devbase は開発コンテナに Docker の制御ソケットを渡す構成を既定に含むため、コンテナ内から他コンテナの環境変数も参照できます
- **構成の展開結果**: `docker compose config` は変数名だけの列挙を実際の値へ解決して表示します
- **利用者権限を得た攻撃者**: 既定の鍵保管では鍵も同時に読めます

既定の保護は「鍵が錠前の隣にある」状態です。端末上で利用者権限を得た攻撃者は防げません。これは実運用上の妥協として明示しています。

## 関連

- [CLI リファレンス: env グループ](cli-reference/03-env.md)
- [環境変数の一覧](environment-variables.md)
- [バンドルの書き出しと取り込み](env-export-import.md)
9 changes: 9 additions & 0 deletions etc/_devbase
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ _devbase() {
'exec:Run a command with the decrypted secrets in its environment'
'encrypt:Move plaintext settings into the encrypted store'
'decrypt:Move encrypted settings back to plaintext'
'rekey:Change who can decrypt the secrets and re-encrypt them'
'doctor:Check for leftover plaintext secrets and ignore-rule gaps'
)

plugin_subcommands=(
Expand Down Expand Up @@ -300,6 +302,13 @@ _devbase() {
'--dry-run[Show what would change without writing]' \
'--yes[Skip the confirmation prompt]' '-y[Skip the confirmation prompt]'
;;
rekey)
_arguments \
'*--add-recipient[Public key to add (repeatable)]:key:' \
'*--remove-recipient[Public key to remove (repeatable)]:key:' \
'--dry-run[Show what would change without writing]' \
'--yes[Skip the confirmation prompt]' '-y[Skip the confirmation prompt]'
;;
keygen)
_arguments \
'--force[Overwrite an existing key]' \
Expand Down
2 changes: 1 addition & 1 deletion etc/devbase-completion.bash
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ _devbase_completions() {
# project / container は同じサブコマンド群 (container は非推奨だが補完は維持)。
local project_subcommands="up down ps login logs scale build rebuild list"
local container_subcommands="up down ps login logs scale build rebuild"
local env_subcommands="init sync list set get delete edit project export import keygen exec encrypt decrypt"
local env_subcommands="init sync list set get delete edit project export import keygen exec encrypt decrypt rekey doctor"
local plugin_subcommands="list install uninstall update info sync repo"
local repo_subcommands="add remove list refresh"
local snapshot_subcommands="create list restore copy delete rotate"
Expand Down
2 changes: 2 additions & 0 deletions issues/plan35.md
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,8 @@ release branch: `release/PLAN35` / base branch: `main`

段階 6 (SOPS を差し替え先として実装) は本 release のスコープ外とし、運用要件が出た時点で別 plan に切り出す。

PR4 では上記に加えて、書き出し・取り込み (`export` / `import`) を秘密ストア経由へ揃えた。移行後の環境で `import` が平文の `.env` を作ると、暗号化ファイルと平文が同時に存在する状態 (§9 でエラーとして停止させる状態) を自分で作り出してしまうため。あわせて取り込み時の控えも暗号文のまま保存されるようになり、§2.3 で挙げた「バックアップに平文が滞留する」経路が塞がる。

### 11.1 依存が直列になる理由

4 本すべてが `lib/devbase/env/` の同一層を触るため、worktree による並行開発の利得よりコンフリクト解消のコストが上回る。PR1 の抽象層が確定しないと PR2 の保存先切替は書けず、PR2 の読み込み経路が確定しないと PR3 の注入経路は書けない。したがって「PR n を release へ merge → PR n+1 を release から切る」の直列で進める。
Expand Down
Loading