上原正吉(EarthLink Network Co., Ltd.)。AI会社・AI社員による業務自動化と、人の逐次介入なしに24時間自動開発を回す仕組みを自社で実践しています。開発ノウハウと具体的な設定方法、運用して分かった改善点を公開しています。

この記事は、連載「dotvault の使い方 ── .env を手で配るのをやめ、CI・チーム・AI エージェントに 1 コマンドで配るまで」の第 5 回です。この連載が終わる時に、dotvault を誰でも無料で使える形で公開する予定です。
新しい人が入った日に、Slack の DM で .npmrc を送る。社内の npm registry のトークンが入ったファイルです。.env は dotvault のような置き場に移しても、.npmrc は手で渡したまま、ということはよくあります。kubeconfig も同じです。クラスタに入るための認証情報が、誰かのノート PC の ~/.kube/config から、別の誰かの ~/.kube/config へ、コピーで渡っていきます。
「.env さえ管理すれば済む」と考えがちですが、key と値に分かれないファイルが、.env と同じくらいあります。そして、そういうファイルほど、うっかり commit に入ります。
この回で分かるのは、.npmrc や kubeconfig も 1 コマンドで預けて、受け取る側は .env と同じ pull か run で受け取れる、ということです。扱うのは、ファイルをそのまま配る dotvault push と、commit する前に secret の混入を見つける dotvault scan です。2 回目で作った demo-app に .npmrc と kubeconfig を push し、別のマシンで pull と run で受け取り、最後に、3 つの secret を含んだ commit を scan が止めるところまで進めます。
dotvault push <ファイル> で登録します。 .npmrc、kubeconfig、YAML の設定のような、中身を丸ごと持ちたいテキストのファイルが対象です。4 回目の import や Upload タブは、ファイルの中の KEY=VALUE を 1 つずつ登録するもので、別の仕組みです。--as .kube/config で受け取る側の置き先を、--pointer KUBECONFIG で run の時に一時パスを入れる環境変数を指定します。.npmrc は何も指定しなくても NPM_CONFIG_USERCONFIG が付きました。dotvault pull か dotvault run です。 pull は .env と一緒に、push したファイルも置き先に書きます。run は一時ディレクトリに書いて環境変数を設定し、コマンドが終わると消します。pull --file は .env.local のような key のファイルを 1 つ受け取る指定です。dotvault scan . は、login しなくても動きます。 AWS のアクセスキー、GitHub のトークン、dotvault のトークンのような、形の決まった secret を探し、見つかれば終了コード 1 で止まります。.git/hooks/pre-commit から dotvault scan . を呼ぶと、commit が止まります。scan は .env の中身の守りではありません。 素の .env の SMTP_PASSWORD=… は拾いません。.env を git に入れないのは .gitignore の仕事で、scan はコードや設定に紛れ込んだトークンを見るものです。demo-app の development に .env(5 key)と .env.local(1 key)がある状態から続けます。4 回目を読んでいなくても、画面の流れは追えます。project-config.json だけを置いた machine-b)で代用しています。ghp_demo… や AKIAIOSFODNN7EXAMPLE は、どこにも使えない文字列です。DATABASE_URL のような名前と値の組、「ファイル」は push で丸ごと登録したものを指します。「pointer」は、run の時にファイルの一時パスを入れる環境変数の名前です。「終了コード」は、コマンドが終わる時に返す数で、0 が成功、それ以外が失敗です。dotvault には、置き方が 2 つあります。
| key | ファイル | |
|---|---|---|
| 単位 | 名前と値の組(DATABASE_URL=…) | ファイルの中身を丸ごと |
| 登録 | 画面の + Add key / Upload、CLI の set / import | CLI の push だけ |
| 受け取り | pull(.env などに書く)/ run(環境変数に入れる) | pull(置き先に書く)/ run(一時ディレクトリに書いて pointer を設定) |
| 画面 | Keys タブ(見る・書く・消す) | Files タブ(一覧・別の環境へコピー・消す。追加とダウンロードは不可) |
| 変更の履歴 | versions で一覧、rollback で前の値に戻す | push のたびに変更の回数が 1 つ増える(画面の VERSION 列) |
| 向くもの | .env、.env.local、1 行ずつ意味のある設定 | .npmrc、kubeconfig、YAML の設定(テキストのファイル) |
.npmrc は KEY=VALUE の形をしていますが、//npm.pkg.github.com/:_authToken=… のような行は key の名前として扱いにくく、ファイルごと置く方が素直です。
demo-app のディレクトリで、.npmrc と ~/.kube/config を push します。
![dotvault push の端末画面(再構成)。cat .npmrc で registry・@acme:registry・_authToken の 3 行、dotvault push .npmrc で「pushed .npmrc (v1) → .npmrc [run sets NPM_CONFIG_USERCONFIG] (project=demo-app, env=development)」、dotvault push ~/.kube/config --as .kube/config --pointer KUBECONFIG で「pushed .kube/config (v1) → .kube/config [run sets KUBECONFIG]」、dotvault files で .kube/config と .npmrc の 2 行(括弧に pointer の名前)。](../assets/DVLT-005/5-1-push.png)
出力の読み方です。pushed .npmrc (v1) → .npmrc は、「.npmrc という名前で v1 を登録し、受け取る側では .npmrc に書く」という意味です。[run sets NPM_CONFIG_USERCONFIG] は、run の時に NPM_CONFIG_USERCONFIG という環境変数に一時パスを入れる、という意味です。.npmrc には何も指定していません。よく使うファイルは、pointer が最初から決まっています。
kubeconfig は、置き先と pointer を自分で指定しました。--as .kube/config で、受け取る側の置き先を project のディレクトリの中の .kube/config に、--pointer KUBECONFIG で、run の時に KUBECONFIG を設定するようにしています。~/.kube/config のように project の外にあるファイルは、--as で置き先を、--pointer で環境変数を、こうして明示しておきます。
dotvault files で、この環境に登録されたファイルの一覧が出ます。「名前 → 置き先 (pointer)」の順です。同じファイルをもう一度 push すると v2 になります。
管理コンソールの project に Files タブがあります。push する前は空で、「Add one from the CLI with dotvault push」とだけ出ます。

ファイルの追加とダウンロードは CLI から行います(追加は push、受け取りは pull か run)。push した後に開くと、一覧になります。

表の cert.der は、9 節で push が text 専用であることを確かめるために入れたもので、この後で消します。.npmrc の VERSION が 2 なのは、撮影の途中で 1 行足してもう一度 push したからです。
「Copy all files in this env…」を押すと、別の環境へまとめてコピーする画面が出ます。

3 回目の Compare envs と同じ考え方です。コピー先に同じ名前のファイルがある時は、チェックを入れない限り上書きしません。この回では Cancel で閉じています(staging に何も作りたくないためです)。
行の Delete を押すと、ブラウザの確認(「Delete development/cert.der? This cannot be undone.」)が出て、OK で消えます。消した後は「Deleted development/cert.der.」の 1 行が出て、一覧が 2 行になります。

受け取る側です。project-config.json だけを置いた空のディレクトリで、dotvault pull を実行します。

pull の出力に出るのは .env と .env.local の 2 行だけですが、ls -la を見ると .npmrc と .kube/config も書かれています。push したファイルは、pull のたびに、置き先に一緒に書かれます。新しく書かれたファイルのパーミッションは .env と同じ -rw-------(本人だけ読み書き)です。
最初の mkdir -p .kube が要る理由は、9 節で説明します。置き先のディレクトリが無いと、pull はファイルを書かずに、同じ出力を返します。
pull でファイルを置くと、そのマシンにファイルが残ります。CI や、短い作業なら、run の方が向いています。

run は、pointer のあるファイルを /tmp の下の一時ディレクトリに書き、pointer の環境変数にそのパスを入れて、コマンドを実行します。コマンドが終わると、一時ディレクトリごと消えます。2 つ目の ls -la が「No such file or directory」なのは、そのためです。
.npmrc の方は、npm config get @acme:registry が https://npm.pkg.github.com/ を返しています。push した .npmrc の 2 行目が、NPM_CONFIG_USERCONFIG 経由で npm に読まれています。project のディレクトリには .npmrc を置いていません。
pointer が無いファイル(9 節の cert.der のような、--pointer も既定の pointer も無いもの)は、run の間だけ置き先に書かれ、終わると消されます。手元に残したいファイルは、pull で受け取ります。
ここからは、配る話ではなく、漏らさない話です。dotvault scan <パス> は、指定したファイルとディレクトリを読み、形の決まった secret を探します。サーバーには何も送りません。login していなくても動きます。撮影の最後に logout して(whoami が「Not logged in.」になる状態で)、secret を直した後のリポジトリに実行しても、login を求められずに「No secrets found.」が返りました。
試しに、3 つの secret を含むリポジトリを作りました。src/s3.js に AWS のアクセスキー、deploy.sh に GitHub のトークン、.github/workflows/ci.yml に dotvault のサービストークンです。node_modules の中にも、同じ AWS のキーを置いてあります。
![dotvault scan の端末画面(再構成)。find . -type f | sort で 5 ファイル(node_modules/some-dep/index.js を含む)、dotvault scan . で .github/workflows/ci.yml:2 [dotvault-service-token]・deploy.sh:2 [github-pat]・src/s3.js:3 [aws-access-key-id] の 3 行(値は est_...ue (72 chars) のように先頭 4 文字と末尾 2 文字だけ)と「3 potential secret(s) found.」、echo $? で 1、dotvault scan src/ で 1 件、machine-b での dotvault scan . で「No secrets found.」と 0。](../assets/DVLT-005/5-4-scan.png)
出力は「ファイル:行 [ルール名] 説明 — 値の一部」の形です。値は、先頭 4 文字と末尾 2 文字と文字数だけが出ます。端末の画面や CI のログに、secret そのものを出さないためです。node_modules の中のキーは出ていません。読まないディレクトリが決まっています(8 節)。
見つかると終了コードが 1、見つからなければ No secrets found. で 0 です。パスを複数渡せます(scan src/ tests/)。
git は、commit の直前に .git/hooks/pre-commit というファイルがあれば実行し、終了コードが 0 でなければ commit をやめます。scan は見つかると 1 を返すので、そこに置くだけで止まります。
![pre-commit フックで commit が止まる端末画面(再構成)。cat .git/hooks/pre-commit で #!/bin/sh と exec dotvault scan . の 2 行、git add src deploy.sh .github .gitignore README.md && git commit で 6 節と同じ 3 件の検出と「3 potential secret(s) found.」、git log --oneline で「fatal: your current branch 'main' does not have any commits yet」、3 つの値を環境変数に置き換えてもう一度 git commit すると「No secrets found.」と「[main (root-commit) f2748e0] add deploy script」「5 files changed, 11 insertions(+)」。](../assets/DVLT-005/5-5-precommit.png)
フックの中身は 2 行です。
#!/bin/sh
exec dotvault scan .
1 回目の commit は、3 件を出して止まります。git log は「commits yet」で、何も記録されていません。3 つの値を process.env.AWS_ACCESS_KEY_ID や ${{ secrets.DOTVAULT_TOKEN }} に置き換えて、もう一度 commit すると通ります。
このフックは、ステージした差分ではなく、作業ツリー全体を読みます。.gitignore で外した .env も読むので、手元の .env に Stripe の本番 key のような形の決まった値があると、commit のたびに止まります。その時と、大きなリポジトリでは、exec dotvault scan src/ .github/ のように .env を含まない範囲に絞ります。チームで共有するなら、.git/hooks ではなく、lefthook や husky のようなフックの管理ツールの設定に同じ 1 行を書きます。
scan の検出は、形の決まった文字列との照合です。0.15.7 の CLI に入っている規則は 10 個です。
| ルール名 | 探すもの |
|---|---|
| aws-access-key-id | AKIA + 16 文字 |
| aws-secret-access-key | aws の近くに引用符で囲んだ 40 文字 |
| private-key | -----BEGIN PRIVATE KEY-----(RSA / EC / OPENSSH / DSA / PGP 付きも。ENCRYPTED PRIVATE KEY は対象外) |
| github-pat | ghp_ + 36 文字 |
| github-fine-grained-pat | github_pat_ + 60 文字以上 |
| slack-token | xoxb- などで始まる文字列 |
| stripe-secret-key | sk_live_ + 24 文字以上 |
| google-api-key | AIza + 35 文字 |
| dotvault-service-token | est_ + 32 文字 + . + 20 文字以上 |
| generic-assignment | secret / token / password / passwd / api_key(api-key・apikey も)の後に = か : と、引用符で囲んだ空白なしの 16 文字以上(大文字小文字は区別しない) |
読まないものも決まっています。
| 読まないもの | 条件 |
|---|---|
| ディレクトリ | node_modules・.git・dist・build・.next・coverage・cdk.out・.turbo |
| 大きいファイル | 5MB を超えるもの |
| binary | 先頭 8KB に NUL 文字があるもの |
.gitignore は見ません。読まないディレクトリは名前で決まっています。
この 2 つの表は、公開されている CLI のコードを読んで書き、ディレクトリ・5MB・binary の 3 つは実測しました。6MB のテキストと、NUL 文字を含む 27 バイトのファイルの両方に AKIAIOSFODNN7EXAMPLE を入れて scan しても、No secrets found. です。
![scan の限界の端末画面(再構成)。ls -la で big.txt(6000021 bytes)と blob.bin(27 bytes)、dotvault scan big.txt blob.bin で「No secrets found.」、cat .env で SMTP_PASSWORD=very-secret-mail-password-2026 と STRIPE_KEY=sk_live_demo…、cat docker-compose.yml で POSTGRES_PASSWORD: "${POSTGRES_PASSWORD_FROM_ENV}"、dotvault scan . で .env:2 [stripe-secret-key] と docker-compose.yml:4 [generic-assignment] の 2 件。](../assets/DVLT-005/5-6-scan-limits.png)
同じ画面の後半が、この回でいちばん伝えたいところです。.env に SMTP_PASSWORD=very-secret-mail-password-2026 と書いても、scan は拾いません。generic-assignment は引用符で囲んだ値だけを見るので、.env の素の書き方は対象外です。同じ .env の sk_live_… は、Stripe の形なので拾います。
つまり、scan は「.env を commit してしまう」事故を止めるものではありません。それは .gitignore に .env を書く仕事です。scan が止めるのは、コードや CI の設定や Dockerfile に、形の決まったトークンを直に書いてしまう事故です。
逆に、docker-compose.yml の POSTGRES_PASSWORD: "${POSTGRES_PASSWORD_FROM_ENV}" は、値ではなく環境変数の参照ですが、引用符の中が 16 文字以上なので拾われています。この形の時は、pre-commit で渡すパスをそのファイルを含まない範囲に絞るか、引用符を外します。
この回で出た失敗と、その時の出力と終了コードをまとめます。

| 操作 | 出力 | 終了コード |
|---|---|---|
置き先のディレクトリ(.kube)が無い状態で pull | .env の 2 行と「2 files synced」だけ。.npmrc も .kube/config も書かれない | 0 |
pull --file .kube/config | unexpected: Invalid env file name | 99 |
pull --file .npmrc | PROJECT_NOT_FOUND(server: No DDB secrets for .npmrc) | 5 |
push .yarnrc(無いファイル) | VALIDATION_ERROR(Could not read … ENOENT) | 6 |
files --env staging(ファイルの無い環境) | No files synced. | 0 |
scan nope/(無いパス) | No secrets found. | 0 |
1 行目が、4 節で mkdir -p .kube をした理由です。置き先にディレクトリを含めるファイルは、受け取る側でそのディレクトリを先に作っておきます。無い時は、push したファイルはどれも書かれません(.kube/config の後の .npmrc も書かれていません)。「2 files synced」の 2 は .env と .env.local の数で、push したファイルは数に入らないので、受け取れたかは ls で確かめます。
2 行目と 3 行目は、push したファイルに pull --file を使った時の出力です。pull --file は .env.local のような key のファイルを 1 つ受け取る指定です。push したファイルは、pull(全部)か run で受け取ります。
最後の行は、パスを打ち間違えても「No secrets found.」で 0 が返る、という出力です。pre-commit に scan src/ と書いて src が無ければ、常に通るので、pre-commit に書くパスは、リポジトリにあるディレクトリかを確かめておきます。
もう 1 つ、push はテキストのファイル用です。

PNG の先頭 1 バイト(0x89)が、文字コードの置き換え文字(3 バイト)に変わっています。push はファイルを文字列として読むので、DER 形式の証明書のような binary は、そのままでは配れません。
6 回目は、複数の project で同じ値を使う key を 1 か所に置き、project ごとに結び付ける Global Keys を、画面と CLI の global で進めます。
dotvault は、.env や設定ファイルの secret をサーバー側に置き、権限のある人・CI・AI エージェントが 1 コマンドで受け取るサービスです(https://dotvault.io)。この連載が終わる時に、dotvault を誰でも無料で使える形で公開する予定です。公開したら、この連載の手順どおりに使い始められます。
興味のある方は、ぜひ「いいね」と記事の購読をお願いいたします。
自社プロダクトは https://www.eln.ne.jp/products にまとめています。
上原正吉。EarthLink Network Co., Ltd. 代表。AI社員に日々の業務を任せ、開発では人が逐一指示しなくても作業が進むHOOTL(Human Out Of The Loop)の仕組みを自社で実践しています。このブログでは、業務自動化と24時間自動開発のノウハウ、具体的な設定方法、日々の運用で見つかった問題と改善を共有します。
また、AIで業務や開発を組み替えたい会社・チーム向けに、AI活用のコンサルティングも受け付けています。ご相談は www.eln.ne.jp からどうぞ。
EarthLink Network は、会社の全業務を AI で回すために、必要になったものを自社で作っています。いま作っているプロダクトの一覧と概要は、こちらにまとめています。
→ EarthLink Network が自社でつくっている18のプロダクト
会社と各プロダクトの詳細は、公式サイト www.eln.ne.jp をご覧ください。