Fundamentals · 9 min read
How to protect your .env file from public access
Last updated
Defense in layers
An environment file should be protected by more than one safeguard. If the document root is configured correctly, the file can't be served. If someone later changes the root, a deny rule still blocks it. If both fail, rotated credentials and short-lived tokens limit the damage. This guide walks through the layers from most to least important and ends with an incident checklist.
1. Keep the file out of the web root
The web server should publish a dedicated directory containing only files meant for browsers: the front controller (index.php, for example), compiled assets and images. Application code, dependencies and configuration live one level above it.
/var/www/example-app/
├── .env ← read by the app, never reachable over HTTP
├── app/
├── config/
├── vendor/ or node_modules/
└── public/ ← the ONLY directory the web server publishes
├── index.php
├── robots.txt
└── assets/On shared hosting that only lets you upload into public_html, upload the application above it and place only the contents of public/ inside public_html. If that's impossible, make the deny rule from step 2 mandatory.
2. Deny dotfiles at the web server
A rule that refuses every path segment starting with a dot also covers backups such as .env.bak, .env.save or .env~, and directories like .git/. Keep /.well-known/ reachable, because it is used for TLS certificates (ACME) and security.txt.
location ~ /\.(?!well-known/) {
deny all;
return 404;
}<FilesMatch "^\.">
Require all denied
</FilesMatch>@dotfiles {
path_regexp /\.
not path /.well-known/*
}
respond @dotfiles 404<configuration>
<system.webServer>
<security>
<requestFiltering>
<denyUrlSequences>
<add sequence="/.env" />
</denyUrlSequences>
</requestFiltering>
</security>
</system.webServer>
</configuration>The server-specific guides explain details and pitfalls: Nginx and Apache. Put the rule in a shared snippet that every virtual host includes, including the plain-HTTP server block, so a new site can't forget it.
3. Restrict file permissions
The file only needs to be readable by the user the application runs as. On Linux, for example:
sudo chown deploy:www-data /var/www/example-app/.env
sudo chmod 640 /var/www/example-app/.env # owner read/write, group read, others nothingPermissions don't stop the web server from serving a file it can read, but they protect against other local users and misconfigured services.
4. Keep deployments clean
- List
.envand.env.*(except.env.example) in.gitignoreand.dockerignore. - Don't copy the whole project into a directory that is served as static files, for example an Nginx container's
/usr/share/nginx/html. - Check that build artifacts, ZIP archives and backups uploaded to storage buckets don't include the file.
- Remember that frontend build tools embed variables with public prefixes such as
NEXT_PUBLIC_orVITE_into JavaScript that every visitor downloads. Never give a secret such a prefix. The Node.js guide covers this in detail.
5. Consider a secrets store
On container platforms and in the cloud, it is often better not to have a .env file in production at all. Inject variables through the orchestrator (Docker/Kubernetes secrets, systemd EnvironmentFile= outside the web root) or load them from a secrets manager at startup. This also makes rotation easier, because values change in one place.
Verify the protection
Test from a machine outside your network. The expected answer is 403 or 404:
curl -sS -o /dev/null -w "%{http_code}\n" https://example.com/.env
curl -sS -o /dev/null -w "%{http_code}\n" https://example.com/.env.production
curl -sS -o /dev/null -w "%{http_code}\n" http://example.com/.envA 200 isn't necessarily a leak, because your site may answer every path with an error page. The IsMyENVPublic check tells these cases apart for you.
If your .env was exposed
Blocking access is only the first step
- Block access immediately with a deny rule, then move the file out of the web root.
- Find out whether it was downloaded. Search your access logs for successful requests:
Shellbash # Nginx / Apache combined log format; zgrep also reads rotated .gz files zgrep -hE '"GET /\.env[^" ]* HTTP/[0-9.]+" 200 ' /var/log/nginx/access.log* | less - Rotate every secret in the file, starting with the most powerful ones: cloud keys, database passwords, payment and mail API keys, the application key and signing secrets. Create new credentials, deploy them, then revoke or delete the old ones at each provider.
- Check provider audit logs for activity with the old credentials, for example AWS CloudTrail, your database logs or your payment provider's API logs.
- Invalidate sessions and tokens signed with rotated keys. For example, changing a JWT secret or Laravel's
APP_KEYlogs users out. - Re-run the check to confirm the fix, and document the incident. Depending on the data involved, you may have legal notification duties, for example under Art. 33 GDPR.
Remediation checklist
- The document root points to a dedicated public directory
- A deny rule for dotfiles is active on every HTTP and HTTPS virtual host
/.well-known/is still reachable for certificate renewal- The environment file is readable only by the application user
.envis excluded from Git, Docker images and build artifacts- No secret uses a public frontend prefix
- After an exposure: every secret was rotated and old credentials were revoked
- The check reports no exposure