A .env file is where your app keeps its secrets: database passwords, API keys, signing tokens. If your website hands that file to anyone who asks, this page walks you through the fix, top to bottom. Confirm it, close it, rotate everything, check the logs.
Check your site now (free scan) Jump to the 60-second checkOpen a private browser window and type your own address with /.env on the end:
https://yoursite.com/.env
A bad response looks like this: a plain page of NAME=value lines. Things like DATABASE_URL=postgres://..., STRIPE_SECRET_KEY=sk_live_..., OPENAI_API_KEY=sk-.... Some browsers download the file instead of showing it; if the downloaded file opens to lines like that, it counts exactly the same. Anyone on the internet can do what you just did.
A good response looks like this: your app's normal "page not found" screen, or a bare 404 or 403 error. That means the file is either not there or the server refuses to serve it. That is what you want.
While you are at it, try the common variants too: /.env.local, /.env.production, and /.env.bak. Backup copies leak just as often as the original.
The thorough version of this check is our free scanner. It looks at your live site passively, needs no signup, and checks this exposure class along with its close cousins (an exposed .git folder, reachable config and backup files, missing security headers). One caveat worth being honest about: a clean check today does not prove the file was never exposed. That is what step 5 is for.
Nobody targeted you. That is the part owners find hardest to believe, and the most useful to understand.
Automated scanners request the same short list of high-value paths from every website they can reach, and /.env is on every list, because one hit pays for millions of misses. The scale is documented: in a 2024 campaign analyzed by Palo Alto Networks' Unit 42, attackers scanned more than 230 million unique targets hunting for exposed .env files, pulled the .env files of around 110,000 domains, and used cloud keys found inside to break into the owners' accounts and demand ransom.
New sites do not get a grace period. Scanners walk the whole public address space, and many also watch public certificate logs, which announce new HTTPS sites shortly after they go live. If your site is reachable, it is in the rotation.
One calming note before you read your logs: a probe is not a breach. You will almost certainly find requests for /.env that got a 404. That is a bot knocking on a locked door. The only entries that matter are the ones where the door opened. Step 5 shows you how to tell the difference.
The root cause is almost always the same: the .env file ended up inside the folder your host publishes. Fix that first, then add the seatbelt.
Vercel serves only the contents of your output directory. If /.env is live, the file got into that folder: usually public/, your build output, or a project deployed as a plain static folder with the .env sitting inside it. Delete the file from the published folder, move each value into the project's Environment Variables settings, and redeploy. The new deployment replaces the old content.
Netlify uploads only your publish directory; files outside it are never deployed. So an exposed .env means it is inside that directory, often because the publish directory is set to the project root. Move the file out (or set a proper publish directory like dist/), put the values into Site configuration, Environment variables, and redeploy.
Same story: Pages deploys only your build output directory. Check that setting; if it points at the project root, everything in the root is public, .env included. Fix the output directory, move secrets into the project's environment variable settings in the dashboard, and redeploy.
If nginx serves your .env, the site's root is pointing at a folder that contains it (usually the project root instead of public/). Fix that if you can. Either way, add this inside the server block as a seatbelt. It blocks every dotfile path (.env, .git, and friends) while leaving .well-known alone, which certificate renewal needs:
location ~ /\.(?!well-known) {
deny all;
}
Then test and reload:
sudo nginx -t && sudo systemctl reload nginx
deny all answers with a 403. If you would rather not confirm the file exists at all, use return 404; in place of deny all;.
Apache's default config protects its own .htaccess and .htpasswd files, but not .env. Add this to your server config or an .htaccess file (Apache 2.4 syntax):
<FilesMatch "^\.">
Require all denied
</FilesMatch>
Then reload Apache. This denies every file whose name starts with a dot. Certificate renewal still works, because the challenge files inside .well-known do not start with a dot themselves. And as with nginx, the deeper fix is pointing DocumentRoot at your public/ folder rather than the project root.
Once the fix is deployed, repeat the 60-second check from step 1. You want that 404 back before you move on.
This is the step people skip, and it is the one that actually protects you. You closed the door, but you usually cannot prove nobody walked through it while it was open. So assume everyone did. Every password, key, and token in that file gets rotated. All of them, not just the scary-looking ones.
Rotate in this order:
The step-by-step console instructions live in our leaked API key runbook, with a per-provider rotation table. Two of the highest-stakes cases have their own deep dives: a leaked Anthropic key and an exposed Supabase service role key (that one bypasses your row-level security, treat it as a database breach until proven otherwise).
Do not wait for the provider to save you. Some providers watch public sources for their own key formats and will email you about a leak, and a few may deactivate a key they find. None of them promise it, and a file sitting on your own website is not where they look first. Rotate by hand, then verify: a test call with the old key should be rejected. Most providers answer a dead key with a 401 authentication error; AWS answers a disabled key with a 403-class error instead. Either way, rejected is what you want to see.
Finally, fix the supply line so the new keys do not leak the same way: keep .env in your .gitignore, keep it out of any folder you publish, and give your deploy environment its values through the host's environment variable settings, never as a file in the site.
You are looking for one thing: requests for /.env that succeeded. A 404 or 403 is a bot bouncing off a locked door. A 200 with a byte count means the file was served, and you should treat its contents as read.
Access logs commonly live at /var/log/nginx/access.log (plus rotated .log.1, .gz siblings). Search them: grep "\.env" /var/log/nginx/access.log*. In each line, the number right after the quoted request is the status. 200 means served; check the timestamp and the requesting IP.
Same search, different path: /var/log/apache2/access.log on Debian and Ubuntu, /var/log/httpd/access_log on Red Hat family systems. Include the rotated files; the exposure may be older than the current log.
Open your project's logs in the dashboard (the Observability section) and filter for the request path /.env. Requests for static files show up as edge requests. Log history is limited, so look now rather than next week.
Netlify exposes traffic logs for static files only through Log Drains, an Enterprise feature. On other plans you cannot see who fetched a static file. That is not a reason to relax; it is the reason step 4 rotates everything.
The dashboard gives you aggregated analytics, not a per-request log of static file fetches. Raw HTTP request logs require Cloudflare's log export tooling on higher plans. If you cannot get them, assume the file was read and let the rotation carry the day.
Note the date. Then check the usage logs of every provider whose key was in the file for activity after that date, using the forensics section of the rotation runbook. And if you find nothing at all? Rotation already made the stolen copy worthless. That is the point of doing it first.
Who wrote this: a security engineer with 20 years in the field who does real responsible-disclosure work on exactly this exposure class, finding publicly reachable .env files and reporting them privately to their owners. The steps above are the same ones those reports recommend. No victim stories here; the owners' details stay private, which is rather the point.
Today you caught this because you went looking (or because something already went wrong). Both are bad alarm systems. Exposures recur: a config change, a new deploy preset, a teammate's shortcut, and the file is back.
Two ways to stay ahead of it. Re-run the free scan whenever you ship something significant. Or let continuous monitoring do it for you: it re-checks your live site around the clock and emails you an alert when something new is exposed, from $49/mo. Plans are on the business page, signup at app.lictor-ai.com/billing.
For the codebase side, the same checks ship in our free, open-source Claude Code plugin. It runs a 48-check audit catalog (exposed env and config files are check number 3) and reports in plain English. Install it with:
/plugin marketplace add Raffa-jarrl/Lictor-AI
/plugin install lictor-security-suite@lictor-ai
And because "trust us" is not evidence: we publish a reproducible benchmark. In a blind run on a 30-case stratified sample of OWASP Benchmark 1.2, the audit scored an F1 of 93.8%. That is a sample, not the full benchmark, and we say so in the results. Building with AI app builders? The Lovable security checklist covers the leaks those tools tend to create, .env exposure included.
Yes. It holds your secrets in plain text: database passwords, API keys, signing tokens. If yoursite.com/.env returns those lines, anyone can read them, and automated campaigns hunt for exactly this file. One 2024 operation documented by Unit 42 scanned over 230 million targets for exposed .env files. Treat every value as stolen and rotate.
Mass scanning, not targeting. Scanners request the same short list of paths, including /.env, from every reachable site, and new sites enter the rotation quickly via address-space sweeps and public certificate logs. Nobody needs to know your site exists. Being online is enough.
Yes, every value. You usually cannot prove which lines were read, and on managed hosts you often cannot see static file access logs at all. Rotate database strings and cloud keys first, then payment and email keys, then AI keys, then the rest. The per-provider steps are in the rotation runbook.