Guide

Laravel .env exposed: fix it, and rotate APP_KEY safely.

Laravel keeps its secrets in .env in the project root, one level above public/ — the only folder a web server should serve. When the document root points at the project root instead, /.env is a download, and with it the database password, APP_KEY, and mail and cloud credentials.

Why it happens

1. Point the document root at public/

# nginx
root /var/www/example/public;

# Apache
DocumentRoot /var/www/example/public
<Directory /var/www/example/public>
    AllowOverride All
    Require all granted
</Directory>

If your host only serves a fixed public_html, put the app beside it (for example ~/example), move the contents of public/ into public_html, and change the two paths in public_html/index.php that load vendor/autoload.php and bootstrap/app.php so they point into ../example/.

Until that’s done, block dotfiles with the rules in the main guide.

2. Rotate, in this order

  1. Database. Change the password of DB_USERNAME and update DB_PASSWORD.
  2. Third-party keys — AWS_*, MAIL_PASSWORD, STRIPE_*, PUSHER_*, anything with a provider behind it. Roll each in the provider’s dashboard and revoke the old one.
  3. APP_KEY last, because of what it touches.

3. Rotate APP_KEY without breaking stored data

APP_KEY encrypts Laravel’s cookies, including the session cookie, and anything stored with Crypt or the encrypted casts; it also signs signed URLs. Whoever holds it can forge all of those. Changing it signs everyone out and makes data encrypted with the old key unreadable, unless you plan for it:

php artisan down
php artisan key:generate --show      # prints a new key; .env is not changed

# .env:  APP_KEY=<new key>   APP_PREVIOUS_KEYS=<leaked key>
php artisan config:cache             # or config:clear if you don't cache config

# Re-save records that use encrypted casts or Crypt, so they are written
# with the new key. Then remove APP_PREVIOUS_KEYS and cache config again.
php artisan up

APP_PREVIOUS_KEYS (Laravel 11 and later) lets the app keep reading old data — but while the leaked key is listed there, values encrypted with it are still accepted. Keep it only until stored data has been re-encrypted, then remove it. If you store no encrypted data, leave APP_PREVIOUS_KEYS out entirely. On Laravel 10 and older there is no such setting: decrypt with an encrypter built from the old key and re-encrypt with the new one.

If storage/logs/laravel.log is public

A document root at the project folder exposes storage/ as well, and with it storage/logs/laravel.log. Error entries record SQL queries with their values, request data, tokens in URLs, and stack traces with server paths. Pointing the document root at public/ (step 1) puts the log out of reach. Then look through it for anything worth rotating, and keep less in it from now on:

grep -niE 'password|secret|token|api[_-]?key' storage/logs/laravel*.log | head -50

Set LOG_LEVEL=warning (or higher) in production so routine debug output isn’t written at all, and remove old log files you don’t need.

4. Turn off debug mode

Set APP_ENV=production and APP_DEBUG=false on every public server. Debug pages print configuration to visitors, and debug tooling has been a direct way in: CVE-2021-3129 let attackers run code through older versions of the Ignition error page when debug mode was on. Keep Telescope, Debugbar and similar tools off public production URLs.

5. Keep it from coming back

Scan your Laravel site