--- title: ".env に入れた API キーがブラウザから見える理由(Vite の VITE_)" description: ".env に入れた API キーは、Vite で VITE_ を付けるとビルド後の JS に焼き込まれ、開いた人に読めます。.env は値をコードから分ける場所で、隠す場所ではありません。届く先を確かめ、鍵をサーバー側へ移し、消えたかまで見る手順。" url: https://ryuuneko.com/blog/env-file-not-secret-vite published: 2026-09-12 20:46:41.92526 updated: 2026-09-12 20:47:36.757366 --- 「API キーは `.env` に入れておけば安全」。半分だけ本当です。 `.env` がやっているのは、値をコードから**分ける**ことで、**隠す**ことではありません。分けた値がリポジトリに乗るのか、ブラウザに届くのか、サーバーに留まるのか。安全かどうかはそこで決まります。Vite で `VITE_` を付けた値は、ビルド後のブラウザ用コードに文字としてそのまま書き写されます。 この記事は、小さな Web ツールを題材に、公開前によく踏むこの形を**再現した記録**です。道具の表示や接頭辞(`VITE_`)は 2026 年 8 月時点の Vite で確認したもので、ツール名やキーの文字列は架空です。前回の [AI で 10 分でアプリを作ったあとに止まった話](/blog/ai-app-in-10-minutes-then-what) の続きになっています。 --- ## まず「はず」を確かめる:ビルドして鍵を検索する 題材は tidy-text という文章整形ツール。「AI で誤字さがし」ボタンが外部の AI を呼ぶので API キーが要り、それを `.env` に入れてあります。コードから外したし、もう見えないはず。 直す前に、その「はず」を確かめます。今どう見えているか分からないと、直ったかどうかも分かりません。公開用にビルドして、ブラウザの開発者ツールでビルド後の JS を開き、鍵の先頭 `key_live` で検索します。 ```js // assets/index-3f9a.js(ビルド後) fetch("https://api.example.com/v1/typos", { headers: { Authorization: "Bearer key_live_9f3a..." } }) ``` 出ました。`.env` のファイル自体は成果物に入っていません。でも**値だけが、ここへ書き写されている**。分けたのに、見えている。 これはまだ手元でビルドしただけなので、公開はしていません。ただ、この形のまま公開すれば、開いた人はこのコードを読めます。 --- ## なぜ見えた? VITE_ は「ブラウザから使っていい値」の印 Vite では、`VITE_` が付いた値をブラウザ側のコードへ出す仕組みになっています。公式ドキュメントにこうあります。`VITE_` で始まる変数はビルド後にクライアント側のソースコードへ露出する。そして `VITE_*` の変数に API キーのような機密情報を入れてはいけない、値はビルド時にソースコードへ焼き込まれる(出典 3)。 つまり `VITE_` は「秘密にする印」ではなく、その逆で「**ブラウザから使っていい値**」に付ける印です。 よくある踏み方はこうです。`.env` に `API_KEY=...` と書く。動かない(`undefined`)。AI に聞くと「`VITE_` を付けてください」と言われる。付けたら動いた。公開する。 動かなかったのは、ブラウザから秘密を読めないように道具が止めていた合図でした。止まった原因を「直した」つもりで、配る側へ鍵を移していたわけです。 ![.env の値がどこへ届くか。リポジトリ・ブラウザ・サーバーの 3 つの届き先と、Vite の VITE_ がブラウザ行きの印であることを示す図](/blog/uploads/env-file-not-secret-vite-where.svg) --- ## .env は何のためにあるのか(dotenv と Twelve-Factor) `.env` を読む dotenv は、`.env` ファイルの値を環境変数(`process.env`)へ読み込むだけのモジュールです。その考え方は Twelve-Factor App の「設定をコードから厳密に分離する」に基づいている、と README 自身が書いています(出典 1、出典 2)。 分ける目的は、手元と公開先で変わる URL や設定を、コードを書き換えずに差し替えることです。鍵にも使えます。ただし、それはあくまで「分ける」であって、**分けた値が最後にどこで読まれるか**までは `.env` は関知しません。 なので、答えはこうなります。 > `.env` は、値をコードから分ける場所。秘密になるかは、最後にどこで使うかで決まる。 --- ## もう一つの届き先:.env 自体が git に入っていないか ブラウザとは別に、`.env` ファイルそのものがリポジトリに乗る経路があります。二つは分けて確認します。 ``` $ git status --ignored --short !! .env ``` `!!` は git の無視対象という印で、今回はファイル自体は入っていませんでした。`.gitignore` が止めるのはファイルのほうで、ブラウザへ出す値までは止めない。この区別ができていれば大丈夫です。 注意点が二つ。`.gitignore` は、すでに追跡しているファイルには効きません(出典 4)。もし追跡済みなら、先に鍵を無効にして作り直すのが先で、履歴の扱いはまた別の話です。もう一つ、道具によっては `.env.local` だけを無視の既定にしていて、素の `.env` は入っていないことがあります。自分の `.gitignore` を一度見ておくと安心です。 --- ## どう直すか:VITE_ を外すのではなく、鍵を読む場所を移す 直し方は「`VITE_` を外す」ではありません。外すと動かなくなるだけで、鍵をブラウザで読もうとしている構造は変わらない。**鍵を読む処理そのものをサーバー側へ移す**必要があります。 自分で書けなくても AI に頼んでいいのですが、「サーバー側に置いて」だけでは、鍵をどう扱うかまでは決まりません。この場で決めた頼み方はこうです。 ``` tidy-text の「AIで誤字さがし」を直して。 秘密の API キーは、ブラウザへ送らず、 サーバー側の環境変数からだけ読んで使って。ログにも出さないで。 ブラウザからは本文を自分の API へ送り、 サーバー側で外部 API を呼んで、結果だけ返して。 修正後、ビルドしたブラウザ側のコードと通信に API キーが残っていないか確認して。 ``` 「何を返さないか」と「直したあと何を確かめるか」まで書いています。返ってきた構成はこうでした。 ``` src/typos.js /api/typos へ本文だけ送る api/typos.js API_KEY を読み、外部 AI を呼ぶ .env API_KEY=key_live_9f3a...(VITE_ が外れた) ``` ブラウザ側は自分たちの窓口へ本文だけ送る。鍵を読むのも、外の AI を呼ぶのも、サーバー側だけ。全部のコードを読めなくても、今回の鍵の文字列と通信先なら追えます。 ![直す前と直した後。ブラウザで鍵を読んでいた構造から、サーバー側の窓口だけが鍵を読む構造へ移り、ビルド成果物と通信の 2 か所で確かめる図](/blog/uploads/env-file-not-secret-vite-fix.svg) --- ## 直したら、成果物と通信の 2 か所で確かめる 「移したつもり」で終わらせないために、実物を 2 か所見ます。 **ビルド成果物に鍵が無いか。** ``` $ rg "key_live_" dist/ (何も出ない) ``` 今回使った鍵は、ブラウザへ配る成果物から消えました。`.env` の変数名を直したからではなく、鍵を読む処理をブラウザ側からサーバー側へ移したからです。置いたファイルではなく、読む場所を見る。この検査は「今回の鍵がこの成果物に無い」ことを確かめるもので、安全性全体を保証するものではありません。 **実際の通信に鍵が載っていないか。** 開発者ツールの Network で、ブラウザから自分の窓口への通信を見ます。 ``` POST /api/typos content-type: application/json { "text": "第1章 はじめに…" } ``` 本文だけ。鍵は載っていません。ブラウザは自分たちの窓口へ本文を送り、外の AI を呼ぶのはその先のサーバー側です。 --- ## 鍵が消えたら終わり? 窓口には先に歯止めを 鍵の置き場所は直りました。ただ、これで終わりではありません。 作った窓口 `/api/typos` は、鍵が無くても呼べます。うちの鍵を直接見なくても、代わりに窓口を使えばいい。鍵は隠れても、窓口は開いたままです。だから公開前の最低限として、決めた回数を超えたら断るようにしておきます。 ``` 21:40 POST /api/typos 200 21:40 POST /api/typos 200 21:41 POST /api/typos 200 21:41 POST /api/typos 429(決めた回数を超えた) ``` 歯止めは動きました。でも、これで全部ではありません。誰に使わせるか、費用の上限、受ける形と返す形。そこは別の記事、[サーバーに移した API キー、まだ終わってない](/blog/api-key-behind-server-not-enough) で扱います。今日は「何も決めずに公開する状態」だけ止めておく、という線です。 隠すことと、使わせすぎないことは、別の仕事です。 --- ## やること 3 つ 1. `.env` ファイル自体が git に入っていないか見る(`git status --ignored`) 2. `VITE_` などブラウザ用の値に秘密が無いか見る(ビルドして鍵を検索) 3. 秘密はサーバー側へ移し、直したらビルド成果物と通信でもう一度確かめる。公開する窓口には先に回数の歯止めを入れる 作業を AI に任せても、この結果は自分で確認できます。`.env` に入れたかではなく、値がどこへ届くか。そこまで見て、やっと出せます。 --- ※ 道具の表示と接頭辞(`VITE_`)は 2026 年 8 月時点の Vite で確認したものです。Next.js など別の道具では接頭辞や既定の挙動が違います。ツール名 tidy-text、鍵の文字列、ログはすべて架空の再現です。「サーバー側に移せば安全」という意味ではなく、置き場所としては直った、までです。 ## 動画版 同じ題材を、YouTube「黒猫開発部」では 2 人がビルド成果物と通信を実際に開きながら 6 分で確かめています。記事は仕組みと頼み方を立ち止まって読む向け、動画は「まず出てしまうところ」を先に見る向けです。 - [.env に入れた鍵、ブラウザから丸見えだった(黒猫開発部)](https://youtu.be/bDbrvM23fzc) - [チャンネル: 黒猫開発部](https://www.youtube.com/@黒猫開発部) ## 出典 1. [dotenv README](https://github.com/motdotla/dotenv/blob/master/README.md) — "Dotenv is a zero-dependency module that loads environment variables from a `.env` file into `process.env`." / "Storing configuration in the environment separate from code is based on The Twelve-Factor App methodology." 2. [The Twelve-Factor App — III. Config](https://12factor.net/config) — "The twelve-factor app requires strict separation of config from code." / "stores config in environment variables" 3. [Vite — Env Variables and Modes](https://vite.dev/guide/env-and-mode.html) — "Variables prefixed with `VITE_` will be exposed in client-side source code after Vite bundling." / "`VITE_*` variables should not contain sensitive information such as API keys. The values of these variables are bundled into your source code at build time." 4. [git — gitignore](https://git-scm.com/docs/gitignore) — "Files already tracked by Git are not affected" / パターンにスラッシュが無ければどの階層でも一致する