Remediation guide
How to fix an exposed .env file
If your .env file is reachable at a URL like https://your-site.com/.env, treat it as an active incident. Automated scanners continuously probe for this path and will scrape credentials within minutes of them going live. This guide gives you the exact steps, in the right order, to stop the bleeding and prevent it from recurring.
- How it happens
- Step 1: Rotate credentials
- Step 2: Remove the file
- Step 3: Block it server-side
- Step 4: Scrub git history
- Step 5: Verify the fix
- Prevention
- FAQ
How a .env file ends up publicly accessible
The most common root causes:
- Deploying the entire project root. You copied or pushed your whole project directory — including the
.env— to the webserver or static host. The server serves every file it can find unless explicitly configured otherwise. - Misconfigured static hosting. Platforms like GitHub Pages, Netlify, and Vercel serve everything in the directory or branch they're pointed at. If
.envis in that directory, it becomes public. - File committed to git. The
.envwas accidentally committed and pushed to the repo powering a deploy-from-repo setup, so it was served alongside your code. - FTP / shared hosting deployment. An FTP client uploaded all files in the project folder, including dotfiles the developer never noticed.
Step 1: Rotate every exposed credential immediately
Do this before anything else. Removing the file does not help a key that has already been scraped — and you should assume it has been. Rotation is the only action that actually invalidates a stolen credential.
Go to each provider's dashboard and revoke or regenerate every secret that appeared in the exposed file:
- AWS — IAM console → Users → Security credentials → Delete the compromised access key, create a new one. If it was a root account key, revoke it immediately and create an IAM user instead.
- OpenAI / Anthropic — Platform dashboard → API keys → Delete the old key, generate a replacement.
- Stripe — Dashboard → Developers → API keys → Roll the secret key (the
sk_live_orsk_test_value). - GitHub tokens — Settings → Developer settings → Personal access tokens → Revoke the token, create a new scoped one.
- Supabase — Project Settings → API → Reset the
service_rolekey. Theanonkey is designed to be public, but reset it too if in doubt. - Database URLs with passwords — Reset the password for the database user referenced in the connection string, then update the URL everywhere it's used (other services, CI/CD secrets, other env files).
- Any other service — Follow the same pattern: revoke first, create a new credential, update all references.
Step 2: Remove the .env from your deployment
With credentials rotated, remove the file from what your server is serving.
- Static hosts (Netlify, Vercel, Cloudflare Pages, GitHub Pages): Remove
.envfrom the repository branch or build output directory the platform is serving, then trigger a fresh deploy. - VPS / shared hosting: Delete the file from the webroot directly (
rm /var/www/html/.env) and redeploy from a clean source.
At the same time, move your secrets off the filesystem permanently. Every major hosting platform has a built-in environment variable UI — use it. Your application code reads from process.env, os.environ, or equivalent at runtime, and the credentials never touch a file that the web server might serve.
Step 3: Block .env access at the server level
Even after the file is gone, add an explicit server-level block. This is defense-in-depth: it prevents a future deploy mistake from re-exposing the file.
Nginx
location ~ /\.env {
deny all;
return 404;
}
Add to your server {} block and reload Nginx.
Apache (.htaccess)
<FilesMatch "^\.env">
Order allow,deny
Deny from all
</FilesMatch>
Works for .env, .env.local, .env.production, etc.
Netlify (_redirects)
/.env /404 404
/.env.* /404 404
Add these lines to your _redirects file in the publish directory.
Vercel (vercel.json)
{
"redirects": [
{
"source": "/.env:path*",
"destination": "/404",
"statusCode": 404
}
]
}
Cloudflare Pages
Cloudflare Pages has no path-level deny directive. The reliable fix is ensuring .env is never included in your build output — confirm it's in .gitignore and that your build command doesn't copy it.
GitHub Pages
GitHub Pages serves files from a branch directly and supports no deny rules. The only correct fix is to ensure .env is never committed to the served branch. Add it to .gitignore and remove it from git entirely (see Step 4).
Step 4: Scrub the .env from git history
If the file was ever committed — even if you later deleted it and added it to .gitignore — it still lives in every historical commit that contained it. Anyone who clones the repo or checks out an old commit can still read the values.
Rewrite the repository history to remove it entirely:
Option A: git-filter-repo (recommended)
# Install (requires Python)
pip install git-filter-repo
# Remove .env from every commit in every branch
git filter-repo --path .env --invert-paths --force
# Force-push the rewritten history
git push origin --force --all
git push origin --force --tags
Option B: BFG Repo-Cleaner (Java)
# Download bfg.jar from https://rtyley.github.io/bfg-repo-cleaner/
# Run against a fresh bare clone of your repo
java -jar bfg.jar --delete-files .env my-repo.git
# Clean up and force-push
cd my-repo.git
git reflog expire --expire=now --all
git gc --prune=now --aggressive
git push origin --force --all
Step 5: Verify the fix is working
After redeploying the corrected build, confirm that the .env path returns 404 and that no secrets remain in any of your served files or bundles.
Verify your site with ExposureCheck →
ExposureCheck will probe /.env, /.env.local, /.env.production, /.git/config, and related paths, then scan your served JavaScript bundle for any hardcoded credentials that may remain. Everything runs in your browser — no signup, no data stored.
You can also do a quick manual check: open an incognito window and navigate directly to https://your-site.com/.env. You should see a 404 or 403. If you see your file contents, the fix has not taken effect — check whether a cache needs purging or whether the file is still present in the deployed directory.
Prevention: stop this from happening again
- Add
.env*to .gitignore before your first commit. Verify it's working withgit status— if the file appears as "untracked" and not "modified," git is correctly ignoring it. - Only deploy your build output, not your project root. Configure your CI/CD or deploy script to publish only the
dist/orbuild/directory. Set this explicitly and verify it in your pipeline. - Use environment variables, not files. Every major hosting platform has a secret/env UI — Netlify, Vercel, Railway, Render, Heroku, Fly.io all support it. Your code reads from
process.env.MY_KEY, and the value is injected at runtime from the platform, never written to a file. - Scan your code for hardcoded secrets before committing. Use LeakCheck — paste your code,
.env, or config and it flags any hardcoded API keys, tokens, or connection strings before they reach the repository. - Re-run ExposureCheck after major deploys. Make it part of your post-launch checklist, especially when you switch hosting platforms or change your deploy script. For a comprehensive checklist of everything to probe — including
.gitfolders, source maps, and database backups — see how to audit a website for exposed files.
Frequently asked questions
How quickly do bots find an exposed .env file?
Within minutes, often faster. Automated scanners continuously probe millions of domains for paths like /.env, /.git/config, and /wp-config.php. If your file was live for more than a few minutes, assume your credentials have been scraped. Log coverage is incomplete and credential-harvesting is automated, so "I don't see unusual access in my logs" is not meaningful evidence that no one found it.
Do I need to rotate credentials even if I don't think anyone found the file?
Yes. There is no reliable way to confirm whether a file was accessed before you removed it. The cost of rotating a credential is 5–15 minutes per service. The cost of leaving a possibly-compromised key active — unauthorized API calls, data exfiltration, unexpected cloud spend — can be far higher. Rotate everything in the file.
I redeployed but ExposureCheck still finds the .env — why?
The most common causes: (1) your CDN or platform has cached the response — purge the cache from your hosting dashboard and wait a few minutes before rechecking; (2) the file is still committed and tracked in your repository — run git ls-files .env and it should return nothing; (3) your deploy script is copying the project root rather than the build output directory — verify the exact directory your deploy step is publishing.
My .env contained a DATABASE_URL with a password. What do I do?
Reset the password for that database user immediately. For hosted databases (Supabase, PlanetScale, Railway, Render), use the platform's dashboard to reset credentials. For self-hosted Postgres or MySQL, use ALTER USER youruser WITH PASSWORD 'new_password';. Then update the connection string in every service referencing it — other environment variable configs, CI/CD secrets, and any other applications using that database.
Doesn't deleting the .env from my repo remove it from GitHub?
No. Deleting a file from the current branch removes it from future clones but leaves it intact in the repository's full commit history. Anyone with read access can check out any previous commit and read the file. To actually expunge it, you need to rewrite history with git filter-repo or BFG and force-push (see Step 4). Even after that, forks may still hold copies of the old commits.
Also in the Copper Bay Labs ship-safety suite
An exposed .env is one of several things to check before and after shipping. These free tools cover the rest:
Paste code or your .env and find hardcoded API keys and secrets before they reach the repo.
Scan your live URL for exposed .env, .git, source maps, and bundled secrets. Free, no signup.
Check your site for ADA/WCAG accessibility and privacy-policy gaps before you ship.