Fragments of verbose memory

冗長な記憶の断片 - Web技術のメモをほぼ毎日更新

Oct 6, 2026 - 日記

envdirx: 1ファイルに1変数、必要な値だけ暗号化する

envdirx: 1ファイルに1変数、必要な値だけ暗号化する

アプリの設定には、ログレベルのような公開しても困らない値と、APIトークンのように秘密にしたい値があります。 envdirx は、こうした設定を「1つの変数につき1つのファイル」で管理し、秘密にしたい値だけ暗号化できるコマンドラインツールです。 小さなシェルスクリプトやローカルで動かすサービスで、設定をファイルごとに管理したいときに使えます。

発想のもとになったのは、dotenvx の「環境変数を暗号化する」というアイデアと、envdir の「1ファイルに1変数」という仕組みです。

保存した設定を、アプリに環境変数として渡す

環境変数は、起動するアプリに設定を渡す方法の1つです。 ログの設定をLOG_LEVELから読むアプリなら、LOG_LEVEL=infoという形で値を渡せます。 認証用のトークンをAPI_TOKENから読むアプリには、API_TOKEN=demo-tokenのように渡します。 変数名は、アプリが読む名前に合わせて設定します。 envdirxは設定を保存する役割と、保存した設定をアプリの起動時に読み込む役割を担います。

保存するときは、ファイル名を変数名、中身を値にします。 普通の設定は平文のまま、秘密にしたい値は公開鍵で暗号化して、同じディレクトリに置けます。 復号に使う秘密鍵の本体は、設定ディレクトリの外に保管します。 次の図は、ログレベルとダミーのAPIトークンを保存した状態の例です。 .envdirx.keyは秘密鍵そのものではなく、外の鍵を指すシンボリックリンクです。

1
2
3
4
5
6
7
試用ディレクトリ/
├── private.key          # 復号に使う秘密鍵の本体
└── env/                 # 設定の保存先
    ├── LOG_LEVEL        # 平文の値: info
    ├── API_TOKEN        # demo-tokenを暗号化した値
    ├── .envdirx.pub      # 暗号化に使う公開鍵
    └── .envdirx.key      # 外のprivate.keyへのシンボリックリンク

アプリを起動するときは、envdirxのrunで設定ファイルを読み、暗号化された値だけを秘密鍵で復号します。 その値を環境変数として渡してから、指定したアプリを起動します。 アプリ側はいつもどおり環境変数を読むだけで、復号処理を追加する必要はありません。 設定ファイルは暗号文のまま残りますが、実行中のアプリには復号した値が渡るので、ログなどへの漏えいには別の対策が必要です。

ファイル名はそのまま、値だけを暗号化する

暗号化しても、ファイル名は変わりません。 たとえばAPI_TOKENを暗号化すると、中身がencrypted:Bで始まるBase64の文字列になります。 ファイルにはAPI_TOKEN=のような変数名を付けず、値だけを保存します。

setは通常、値を平文で保存します。 暗号化したい場合だけ、set -c API_TOKENのように-cを付けます。 runは平文と暗号文の両方を読み込めるので、既存のenvdirを一度に全部暗号化する必要はありません。 ただし、envdirx:とencrypted:は暗号文を見分けるための接頭辞です。 この文字列で始まる値は平文として保存できません。

dotenvxとは保存形式も鍵の形式も異なるので、そのまま読み込めません。 .envにまとめて管理したいならdotenvx、変数ごとにファイルを分けたいならenvdirx、という選び方ができます。 dotenvxの使い方は以前の記事「dotenvx: 環境変数管理を快適にするツール 」にも書いています。

平文と暗号文を保存して、コマンドに渡す

以下の例は、macOS のPOSIXシェルとenvdirx 0.1.0で動作を確認しました。 本物のAPIトークンやパスワードを使わず、ダミーの値で試します。

PyPIのenvdirx をuv でインストールします。 envdirx --versionで0.1.0と表示されれば準備完了です。

1
2
uv tool install envdirx
envdirx --version

1. 保存先を作り、秘密鍵を外に置く

mktempで試用ディレクトリを作り、その中のenvを設定の保存先にします。 続いてkeygenで、envの外に秘密鍵、envの中に公開鍵と秘密鍵へのシンボリックリンクを作ります。

1
2
3
DEMO=$(mktemp -d)
envdirx -d "$DEMO/env" mkdir
envdirx -d "$DEMO/env" keygen -k "$DEMO/private.key"

秘密鍵の本体はprivate.keyで、.envdirx.keyはその絶対パスを指すシンボリックリンクです。 この段階では、冒頭の図のうち鍵のファイルとenvディレクトリが作られています。 新しく作るenvの権限は0700、秘密鍵は0600、公開鍵は0644です。

-dはサブコマンドより前に置きます。 省略すると、カレントディレクトリの./.envsを使います。 ほかのコマンドは保存先を自動で作らないので、初回はmkdirを実行してください。

この例では-kで秘密鍵のファイル名を指定しています。 既存のディレクトリを-Kで指定することもできます。 秘密鍵、公開鍵、シンボリックリンクがすでにある場合、keygenは上書きしません。

2. ログレベルは平文、APIトークンは暗号化して保存する

LOG_LEVELには平文でinfoを保存し、API_TOKENにはdemo-tokenを暗号化して保存します。 値は標準入力から読み込ませ、runで実行したシェルの中で元の値と一致するか確かめます。 printfに直接書いているのは、どちらもダミーの値です。

1
2
3
4
printf '%s' 'info' | envdirx -d "$DEMO/env" set LOG_LEVEL
printf '%s' 'demo-token' | envdirx -d "$DEMO/env" set -c API_TOKEN
envdirx -d "$DEMO/env" run -- sh -c \
  'test "$LOG_LEVEL" = info && test "$API_TOKEN" = demo-token'

runは終了コード0を返し、両方の値が元どおり渡されたことを確認できました。 保存先ではenv/LOG_LEVELが平文、env/API_TOKENが暗号文のままです。

本物のトークンやパスワードをコマンドに直接書くと、シェルの履歴などに残ることがあります。 実際に使うときは、アクセス権を制限したファイルなどから標準入力に渡してください。

runの--は必須です。 後ろに続く引数は、すべて実行するコマンドに渡ります。 sh -cの中身をシングルクォートで囲んでいるのは、呼び出し元のシェルで先に変数が展開されるのを防ぐためです。

runの終了コードは、実行したコマンドと同じになります。 sh -c 'exit 7'を渡して試すと、runも終了コード7を返しました。 既存のシェルスクリプトでも、そのまま終了コードでエラーを判定できます。

保存済みの値も、1ファイルずつ暗号化できる

平文で保存してある値は、encryptで暗号化できます。 先ほどのディレクトリにダミーのDB_PASSWORDを追加し、このファイルだけを暗号化してみます。 最後にrunで、元の値を読み出せることを確かめます。

1
2
3
4
printf '%s' 'demo-password' | envdirx -d "$DEMO/env" set DB_PASSWORD
envdirx -d "$DEMO/env" encrypt DB_PASSWORD
envdirx -d "$DEMO/env" run -- sh -c \
  'test "$DB_PASSWORD" = demo-password'

暗号化後もrunから元の値を使えました(終了コード0)。 すべての平文ファイルを暗号化したいときは、encrypt --allを使います。 ファイル名か--allの指定が必須なので、何も指定せずに実行しても全ファイルが変換されることはありません。

すでに暗号化したファイルは、名前を指定するとエラーになりますが、--allならスキップされます。 複数のファイルを扱う場合は、全部を読み込み、検証と暗号化を済ませてからファイルに書き込みます。 ただし、書き込み自体は1ファイルずつ行うため、途中で失敗しても、それまでに書き込んだ分は元に戻りません。

移行前には元データを安全な場所にバックアップし、移行後に実際のコマンドを動かして確認してください。 暗号化したからといって、以前の平文がディスクの履歴やバックアップから消えるわけではありません。

runとgetは値の扱いが違う

runが値を読み込むときのルールも、envdirに合わせています。 ファイルの中身が、そのまま環境変数に入るとは限りません。

  • 0バイトのファイル:同名の環境変数を削除します。
  • 中身のあるファイル:最初の行だけを使い、その行の末尾の空白とタブを除きます。
  • 値の中のNUL:改行へ変換します。

空文字を設定したい場合は、0バイトではなく改行1バイトを保存します。 実行したコマンドでは、0バイトのファイルなら変数が削除され、改行1バイトなら空文字になりました。 複数行を渡した場合も、runが使うのは最初の行だけです。

get NAMEは、保存した元の値を取り出すコマンドです。 暗号文なら復号して、末尾に改行を足さずに標準出力へ書き出します。 確認した例でも、複数行の値や末尾の空白はそのまま残りました。 トークンなどの中身も端末やログに出てしまうので、値が保存されているか調べたいだけなら使わないでください。

decrypt NAMEは、暗号化したファイルを平文に戻します。 復号した値をディスクに書き戻す操作なので、getとは用途が異なります。 コマンドを動かすだけならrunでよく、先にdecryptする必要はありません。

暗号化で守れる範囲と、鍵の管理

暗号化で守れるもの、守れないもの

envdirxで暗号化すると、保存した値は秘密鍵がなければ読めなくなります。 ただし、実行するときには復号した値をプロセスに渡します。 実行中のプロセスや実行ユーザーは、その値にアクセスできます。 アプリが環境変数をログに書けば、そこから漏れる可能性もあります。

暗号化にはX25519による鍵共有と、ChaCha20-Poly1305による認証付き暗号を使っています。 改ざんを検出するための認証には、ファイル名も含まれます。 試しに暗号化したAPI_TOKENを別の名前のファイルにコピーすると、runは復号に失敗して終了コード111を返しました。 このとき、指定したコマンドは起動しませんでした。 変数名を変えるときは、ファイル名だけを変えるのではなく、新しい名前で値を登録し直してください。

暗号化には公開鍵、復号には秘密鍵を使うため、公開鍵だけを持つ環境でも新しい値を暗号化して登録できます。 公開鍵では復号できませんが、保存先に書き込める人は、新しく暗号化した値で設定を差し替えられます。 値を登録するために必要な権限と、復号して使うために必要な権限は異なります。

今のバージョンは、ローカルのPOSIX環境で使うことを想定しています。 悪意のあるユーザーが同時に保存先を書き換える場合の対策や、鍵を定期的に交換する機能はありません。 設定を集中管理したい場合や、利用者ごとに権限を分けたい場合は、envdirx以外の仕組みも必要です。

秘密鍵を保管し、配布物には含めない

秘密鍵の本体は、envdirの外に通常のファイルとして保存します。 所有者はコマンドを実行するユーザーにし、グループやほかのユーザーにはアクセス権を与えないでください。 試しに鍵の権限を0600から0644に変えると、runは終了コード111を返し、実行を拒否しました。 秘密鍵を失うと復号できなくなるので、実際に使う鍵は別の安全な媒体にも保管しておきます。

設定ディレクトリを配布するときは、.envdirx.keyにも注意してください。 cp -L、tar -h、rsync -Lのようにシンボリックリンクをたどる操作をすると、外に置いた秘密鍵までコピーしてしまうことがあります。 配布するファイルから.envdirx.keyを除き、配布先でリンクを作れば、秘密鍵の混入を避けられます。 公開鍵と暗号文はどこに置くか、秘密鍵はどこで保管するか、何をバックアップに含めるかを、それぞれ決めておきましょう。

試用ディレクトリの後片付け

試し終えたら、作ったディレクトリごとダミーの値と秘密鍵を削除します。 実際に使っている鍵や設定を削除しないよう、実行前にDEMOがこの手順で作った試用ディレクトリを指していることを必ず確認してください。

1
rm -rf "${DEMO:?}"

まずは1つの変数で試してみる

まずはAPIトークンなど1つの変数だけを暗号化し、いつものコマンドがrun経由でも動くか試してみてください。 設定をコピーするときは、秘密鍵を含めないよう注意が必要です。