結論
- サーバーの「中」で、公開ディレクトリ(Nginx の
root)に.envや.gitが残っていないかを探します。 - 手元のPCから「外」へ、
/.envと/.git/configを実際に取りに行きます。200が返ったら、中身を取られる状態です。 - Nginx でドットファイルを
denyしても、location ^~ /static/のような設定があると、その配下ではdenyが効きません。筆者の検証環境でも403→200に戻ることを確認しました。閉じたあとは、必ずもう一度「外」から確かめてください。
この記事は、VPS連載の3本目です。1本目は UFWで閉じていてもDockerの公開ポートは別扱い、2本目は AIで作ったアプリをVPSに載せた直後に見る10項目 です。今回も「中」と「外」の両方で確かめる考え方で進めます。
検証環境
- nginx/1.26.3(Debian 13 の nginx パッケージ)
-
127.0.0.1だけで待ち受ける使い捨ての設定で試しました。外には公開していません。 - 置いたファイルはすべてダミーです(
.envの中身はDUMMY_API_KEY=dummy-not-a-real-key-000のような値)。検証後に設定・ファイルとも削除しました。
実際の VPS(Ubuntu など)や、ディストリビューションが用意する既定の設定ファイルでは試していません。結果は、下に載せる設定での動きです。
なぜ、AIで作ったアプリで起きやすいのか(推測)
ここは筆者の推測です。
-
git cloneしたフォルダを、そのまま Nginx のrootにしてしまう(.gitも一緒に公開される) - ビルド成果物と同じフォルダに
.envを置いてしまう - 作業中の
*.bakや DB ダンプ(*.sql)、ソースマップ(*.map)が残る
1. 中で探す
サーバーの中で、公開ディレクトリを探します。パスは自分の root に合わせてください。
sudo find /var/www -maxdepth 4 \( -name '.env*' -o -name '.git' -o -name '*.sql' -o -name '*.bak' -o -name '*.map' \)
ダミーのフォルダで試すと、次のように見つかったパスだけが出ます。中身は表示しません。
/www/app/.env
/www/app/.env.production
/www/app/.git
/www/app/db.sql
/www/app/dist/main.js.map
/www/app/x.bak
root が複数ある場合は、筆者が作っている読み取り専用のCLI vps-check の WEB-002 でもまとめて点検できます。Nginx の設定にある静的な root / alias の配下から、環境ファイル・Git のメタデータ・DB ダンプ・バックアップ・アーカイブ・ソースマップを探し、パスとサイズだけを表示します。README に書いているとおり、次の点は WEB-002 の対象外です。
- ファイルの中身は読みません
- Nginx のアクセス制御(
denyなど)は評価しません - 外から取れるかどうかは確かめません
つまり、WEB-002 で見つかっても、外から取れるとは限りません。逆に deny で閉じたつもりでも、取れる場合があります(後述)。だから次の「外」の確認が必要です。
2. 外から取る
自分のサーバーに対してだけ、手元のPCから実行します。example.com は自分のドメインに置き換えてください。
curl -s -o /dev/null -w '%{http_code}\n' https://example.com/.env
curl -s -o /dev/null -w '%{http_code}\n' https://example.com/.git/config
Nginx の既定のままの静的配信(root を指定して try_files $uri $uri/ =404; だけ)で試すと、ダミーの .env も .git/config もそのまま返りました。
$ curl -sI http://127.0.0.1:18061/.env
HTTP/1.1 200 OK
Server: nginx/1.26.3
Content-Type: text/plain
Content-Length: 39
Nginx は、ドットで始まるファイルを特別扱いしません。root の下にあれば、ほかのファイルと同じように返します。
200 が返ったら、中身を取られる状態です。すぐ次の章で閉じてください。鍵はもう漏れたものとして、再発行を先にするのがおすすめです(筆者の提案)。
3. Nginx で閉じる
server ブロックの中に、ドットで始まるパスを拒否する location を足します。.well-known は証明書の更新(ACME)で使うため、除外しています。
server {
root /var/www/app;
location / {
try_files $uri $uri/ =404;
}
location ~ /\.(?!well-known) {
deny all;
}
}
この設定での結果です。
/.env 403
/.git/config 403
/.git/HEAD 403
/static/.env 403
/static/app.css 200
/.well-known/acme-challenge/test 200
$ curl -sI http://127.0.0.1:18062/.env
HTTP/1.1 403 Forbidden
Server: nginx/1.26.3
deny の行は、access モジュールの規則です。規則は上から順に調べられ、最初に当てはまったもので決まります(nginx 公式 ngx_http_access_module)。
4. 落とし穴:^~ があると deny が効かない
Nginx は、リクエストに使う location を次の順で選びます(nginx 公式 ngx_http_core_module の location)。
- 文字列(prefix)の location の中から、一番長く一致するものを選んで覚えておく
- 正規表現の location を、設定ファイルに書かれた順に調べ、最初に一致したものを使う
- どの正規表現にも一致しなければ、1 で覚えた prefix の location を使う
ただし、公式には「一番長く一致した prefix location に ^~ が付いていると、正規表現は調べない」とも書かれています。
静的ファイルのキャッシュ設定などで、次のような location を足すことはよくあります。
location ~ /\.(?!well-known) {
deny all;
}
location ^~ /static/ {
expires 7d;
}
/static/.env に一番長く一致する prefix は ^~ /static/ です。そのため、正規表現の deny は調べられません。実際に試すと、/static/.env だけが 200 に戻りました。
/.env 403
/.git/config 403
/.git/HEAD 403
/static/.env 200 ← すり抜け
/static/app.css 200
$ curl -sI http://127.0.0.1:18063/static/.env
HTTP/1.1 200 OK
Server: nginx/1.26.3
Content-Type: text/plain
Content-Length: 39
Expires: Sat, 24 Oct 2026 04:20:25 GMT
Cache-Control: max-age=604800
location の書き順を入れ替えても、この結果は変わらないはずです。^~ の判定は、正規表現を調べる前に行われるからです(公式の当たり順から導いた推測で、入れ替えは試していません)。
直し方(どちらも筆者の環境で 403 を確認)
A. ^~ の中にも deny を入れ子で書く
location ^~ /static/ {
location ~ /\. {
deny all;
}
expires 7d;
}
結果は /static/.env → 403、/static/app.css → 200 でした。location は入れ子にできます(公式)。
B. ^~ を外して、ただの prefix にする
location /static/ {
expires 7d;
}
これでも /static/.env は 403 になりました。ただし ^~ を外すと、location ~* \.(css|js)$ のような正規表現の location があるときに、/static/ の下でもそちらが使われるようになります(公式の当たり順から)。もとの設定の意図が変わることがあるので、A のほうが影響は小さいと考えています(筆者の判断)。
ほかにも ^~ を使った location がないか、設定をまとめて確認できます。
sudo nginx -T 2>/dev/null | grep -n '\^~'
5. 一番いい対策は、置かないこと(筆者の提案)
deny は保険です。設定を足すたびに、今回のようなすり抜けが起きる余地があります。
-
rootは、ビルドの成果物(dist/など)だけを指すようにする -
.envはアプリ側の、rootの外に置く -
.gitのあるフォルダをrootにしない。デプロイは成果物だけをコピーする
6. もう公開されていたら(筆者の提案)
- 鍵・パスワードは、先に再発行する(ファイルを消しても、取られた値は戻りません)
- そのあとファイルを消して、「外」から
404か403になることを確かめる - アクセスログで、
/.envや/.git/への200の応答があったかを確認する
AIのエージェントに秘密情報を読ませない話は、AIエージェントに.envを読まれていないか、ファイル名だけで点検する日本語CLIを作った にまとめています。
まとめ
- 中で
find(または vps-check の WEB-002)を使って探し、外からcurlで取りに行く。 - Nginx の既定の静的配信では、
.envも.git/configもそのまま200で返る(筆者の環境で確認)。 -
location ~ /\.(?!well-known) { deny all; }で403になる(筆者の環境で確認)。 - ただし、一番長く一致した prefix に
^~があると正規表現は調べられず(公式)、その配下では200に戻る(筆者の環境で確認)。 - 閉じたら、必ずもう一度「外」から確かめる。
関連記事:
※ nginx の仕様は、2026-10-17 時点の公式ドキュメントに基づきます。検証は nginx/1.26.3 で、ローカルの使い捨て環境でのみ行いました。
更新情報やちょっとしたTipsは X(@supportapp3_lab)で発信しています。