Securealy
Check · Security · blocker · verified

No env files served by URL

/.env, /.env.local, /.env.production and .bak variants return 404.

Severity
blocker (one caps the grade at D, two at F)
Tier
Verified: after you prove ownership
Category
Security
Fix
Free, in full, below
Id
exposed-env-files

What this means

Passing means /.env, /.env.local, /.env.production and their .bak and similar variants all return 404 on your domain. Failing means at least one returns 200, which puts the file's contents, usually every secret your app holds, one URL away from anyone who guesses the name. People guess the name constantly.

How we test it

This check runs only after you verify ownership with a DNS TXT record named _securealy, a securealy-verify meta tag, or /.well-known/securealy.txt; we never probe these paths on an unverified domain. We then request /.env, /.env.local, /.env.production, /.env.local.bak and a handful of similar names, once each. Pass is 404 on every path. Fail is any 200, and the check is a launch blocker.

Why it matters

One of these being a 200 is the most common total compromise. An env file holds the keys that bypass every other defence: the database service key, the payment secret, the email key. In the Fix Kit's own audit a .env.local.bak had been committed with the hosting, GitHub, database-management and email tokens in it; the ignore rule was .env*.local and the backup had a date suffix. An ignore rule is a claim. A request to the URL is the proof.

The fix

If any path returned 200, treat every value in that file as leaked and rotate first. The order and the per-provider steps are in templates/rotate-leaked-credential.md (Module 3): create the new credential first, set it everywhere with printf %s 'VALUE' | vercel env add NAME production (never echo), deploy, revoke the old one, then prove the old value gets 401 or 403 with one request. A human does the dashboard steps.

Then stop the whole family from shipping, at every level:

  1. Ignore the family in git, not one file name. The kit's block also keeps a placeholder-only .env.example tracked if you want one:
cat >> .gitignore <<'G'
.env
.env.*
*.bak
*.bak-*
G
git ls-files | grep -E '\.env|\.bak' ; echo "---- tracked env or bak files above; expect none"

If anything is listed, remove it from tracking with git rm --cached PATH and commit. Say plainly in your notes that it remains in git history; rotation is the real fix and a history rewrite is optional after that.

  1. Keep internal files out of the deploy as well, because the hosting provider serves every committed file at its repo path unless told not to:
cat >> .vercelignore <<'V'
/docs/
**/scripts/
**/*.sql
**/*.template
**/*.bak
CLAUDE.md
**/CLAUDE.md
AGENTS.md
**/AGENTS.md
V

Anchor root rules with a leading slash. An unanchored docs/ matches api/docs/ too and will remove a function.

  1. Treat *.template and *.example as shipping files: placeholders like REPLACE_ME only, never values.

  2. Push, wait for the deploy to show Ready, then verify by fetching the URLs. One request each, no loops:

curl -s -o /dev/null -w "%{http_code}\n" "https://YOUR_DOMAIN/.env.local"
curl -s -o /dev/null -w "%{http_code}\n" "https://YOUR_DOMAIN/.env.local.bak"
bash MODULE-03-lock-it-down/templates/audit.sh files

Expect 404 from both and files: 0 reachable from the script. If a path still returns 200, the ignore file is not deployed, the pattern is wrong, or the build failed and the old deploy is still live. Check the deploy log, not the deploy status.

A deeper runbook for this class is in the Fix Kit, Module 03.

Prompt for your AI

Paste this into the coding agent that built the app.

Run `git ls-files | grep -E '\.env|\.bak'` and show me the output; list every tracked env or backup file.
Read .gitignore and .vercelignore (create them if missing) and add the whole family: .env, .env.*, *.bak, *.bak-*, and the deploy exclusions from Module 3 step 5b, with root rules anchored by a leading slash.
For every tracked env or backup file, run `git rm --cached PATH` and tell me which values it contained by variable NAME only, never the value.
STOP. I will rotate each of those credentials at the provider dashboards myself before anything is deployed; wait for my confirmation.
After I confirm, commit and push, and wait for the deploy to show Ready; never poll in a loop.
Then run once each: curl -s -o /dev/null -w "%{http_code}\n" https://YOUR_DOMAIN/.env.local and the same for /.env.local.bak and /.env, and show me the output; expected: 404 for every path.

Next

Grade your own URL

Every grade lists this check with its evidence for your site. Two free grades a day.