Docs

Block public .git and .env files

Block public .git and .env files

When https://example.com/.git/HEAD or https://example.com/.env opens in a browser, the site is giving away what should never leave the server. The .env file holds the database password, mail and payment keys and the application's encryption key. The .git folder holds the whole source code with its history, including every password that was ever committed and later "removed". Free tools download a public .git folder in minutes, and bots scan the whole internet for both paths every day.

This is the one item on the site checklist that should not wait until tomorrow. If it is red, assume someone already has the files.

Treat it as a leak, not a setting

Blocking the files stops the next download, not the ones that already happened. Do it in this order:

  1. Block access now. The quickest way is a rule on the web server or in Cloudflare (see the tabs below). It takes a minute.
  2. Change every secret that was in .env: the database password, SMTP and mail API keys, payment provider keys (Stripe, PayPal), cloud storage keys, OAuth client secrets, API tokens. Create new ones at each provider and revoke the old ones.
  3. Do the same for secrets in the git history if .git was public. Anything ever committed counts, even if a later commit deleted it.
  4. Check the logs to see whether someone fetched the files and since when.

Warning: In Laravel, APP_KEY encrypts sessions, cookies and any data stored with Crypt or encrypted casts. A new key logs everyone out, and encrypted data in the database can no longer be read with the new key alone. Laravel 11 and newer can read old data while you move to a new key: put the old key in APP_PREVIOUS_KEYS.

To see who downloaded the files, search the access logs (rotated logs included):

sudo zgrep -hE '"GET /\.(env|git)' /var/log/nginx/access.log* /var/log/apache2/access.log* 2>/dev/null | grep '" 200 '

Every line with status 200 is a successful download. On shared hosting, look for the same paths in the panel's raw access logs.

Check it yourself

Replace example.com with your site:

curl -s -o /dev/null -w '%{http_code}\n' https://example.com/.git/HEAD
curl -s -o /dev/null -w '%{http_code}\n' https://example.com/.env

403 or 404 means blocked. 200 means the file is public; to see what a visitor sees, run the same command without -o /dev/null -w …. A .git/HEAD that starts with ref: refs/heads/ is a real repository.

Check http:// and the www variant of the address too, in case one of them is served by a different configuration.

How to fix

There are two levels. A rule that blocks paths starting with a dot is fast and stops the leak today. The lasting fix is to keep both out of the web root: the web server should only ever see the folder meant for the public (public/, public_html/), and the deploy should not copy .git there at all.

nginx

Add this to every server { … } block of the site, above the other location blocks: on Debian and Ubuntu in /etc/nginx/sites-available/example.com, on Rocky, Alma and CentOS in /etc/nginx/conf.d/example.com.conf:

# Every path that starts with a dot, except /.well-known/.
location ~ /\.(?!well-known/) {
    deny all;
}

nginx uses the first regular-expression location that matches, so this one has to come before rules like location ~ \.php$.

sudo nginx -t && sudo systemctl reload nginx

Apache and LiteSpeed

Add this at the top of the .htaccess in the site's root folder, or inside the site's <VirtualHost> if you manage the server:

# Every path that starts with a dot, except /.well-known/.
RedirectMatch 404 /\.(?!well-known/)

It answers 404, as if the files did not exist. It works on Apache and on LiteSpeed. <FilesMatch "^\.env"> is not enough: it matches file names only, and inside .git the files are called HEAD, config and so on.

With server access, reload after the change:

sudo apachectl configtest && sudo systemctl reload apache2

(httpd instead of apache2 on Rocky, Alma and CentOS.)

Laravel

The web server's document root must be the public/ folder, never the project folder. Then .env, .git, vendor/ and storage/ all sit outside what the server can serve, and no extra rule is needed. Laravel Forge, Ploi and Laravel Cloud set this up by default.

On shared hosting where the document root is fixed to public_html, do not upload the whole project into public_html. Upload it next to it (for example ~/example-app) and point the document root at its public folder in the panel (see the cPanel and Plesk tabs). If the panel does not allow that, replace public_html with a link to it over SSH:

mv ~/public_html ~/public_html.old
ln -s ~/example-app/public ~/public_html

Ask the host first if you are not sure links are allowed. Until the move is done, the .htaccess rule from the Apache tab is the stopgap.

cPanel

Quick block: add the RedirectMatch line from the Apache tab to .htaccess in public_html (File Manager, with Show Hidden Files on).

Move the document root for an addon domain or subdomain: Domains → Domains, Manage next to the domain, change New Document Root to the folder with the public files (for example example-app/public) and Update. The main domain's document root is fixed to public_html; for it, use the link from the Laravel tab or ask the host.

Deploying with Git Version Control: clone the repository outside public_html and copy only the public files into it with a .cpanel.yml deployment file. A repository cloned straight into public_html publishes its .git folder.

Plesk

Quick block: Websites & Domains → example.com → Apache & nginx Settings, in Additional nginx directives add:

location ~ /\.(?!well-known/) {
    deny all;
}

Move the document root: in the domain's Hosting Settings (in newer Plesk versions Hosting & DNS → Hosting), set Document root to the public folder, for example httpdocs/public for Laravel or Bedrock, and save.

If you deploy with Plesk's Git extension, set its deployment path to the document root and keep the repository itself outside it.

Cloudflare

If the site runs behind Cloudflare (orange cloud), a WAF rule blocks both paths in a minute, on the free plan too:

  1. Open the domain and go to Security → WAF → Custom rules (in the newer dashboard Security → Security rules) and create a rule.
  2. Name it Block .git and .env, click Edit expression and paste:
(http.request.uri.path contains "/.git" or http.request.uri.path contains "/.env")
  1. Choose the action Block and Deploy.

This only protects traffic that goes through Cloudflare. Anyone who knows the server's IP address can still ask it directly, so fix the server too.

IIS and ASP.NET

IIS can hide whole path segments. In the site's web.config:

<configuration>
  <system.webServer>
    <security>
      <requestFiltering>
        <hiddenSegments>
          <add segment=".git" />
          <add segment=".env" />
        </hiddenSegments>
      </requestFiltering>
    </security>
  </system.webServer>
</configuration>

Requests for these paths get 404. By default IIS also refuses files without a known MIME type, so a public .env usually means someone added a catch-all mimeMap; remove it.

Node.js and Next.js

Next.js serves only the public/ folder, so .env and .git leak only when something in front of it (nginx, Apache) points at the project folder. Fix that server's document root or add the rule from its tab.

In Express, serve a dedicated folder and never the project root:

app.use(express.static(path.join(__dirname, 'public')));

express.static(__dirname) publishes everything next to the code, .env and .git included.

Deploy without .git

Even with a rule in place, the safest .git is one that is not on the web server at all. Instead of git pull in the web root, export the files:

# Only the files from the last commit, without .git:
git archive --format=tar HEAD | ssh deploy@example.com 'tar -x -C /var/www/example.com'

# Or rsync, leaving out .git and the local .env:
rsync -az --delete --exclude='.git' --exclude='.env' ./ deploy@example.com:/var/www/example.com/

--delete removes files on the server that are not in your folder, so check the target path twice. Keep the server's .env outside the folder you sync, or exclude it as above.

Never block /.well-known

The rules above deliberately leave /.well-known/ open. Let's Encrypt checks it when it issues and renews certificates, and files like security.txt and the app links for iOS and Android live there. A rule that blocks every dot path also stops certificate renewal, and the site goes down when the certificate expires.

Confirm the fix

Run the two curl commands from Check it yourself again, then open the target in PulseKeeper and click Check now on the Checklist tab. The item turns OK within a few minutes instead of at the next daily run. Blocking does not undo the leak: finish rotating the secrets even after the item is green.