Skip to content
>_ITDITDWeb Security Platform

By Stack

#shared hosting#Laravel#env#Apache

Keeping .env off the public web on shared hosting

Make .env, .git, and vendor unreachable when you deploy Laravel-style apps on shared hosting. The real fix: app body outside the web root, expose only public/. Placement diagram, .htaccess first aid, the restructure, ITD's bootstrap-redirect trap, and self-check.

Published 2026-06-07 Updated 2026-06-07 3 min read

For: anyone running Laravel-style apps (where only public/ should be exposed) on shared hosting with Apache/.htaccess. Distilled from a real incident (→ .env exposed to the world) — for securing your own deployment.

ITD's view: this isn't 'one person's mistake'

Early shared-hosting culture put everything directly under public_html. Laravel (which expects only public/ exposed) flowed into that, and a dangerous layout became the de-facto standard — even vendors' own docs put projects inside public_html. So the fix is process, not vigilance: change your deployment default.

Get the placement right (diagram)

✗ Dangerous (body at the public root)

public_html/
├─ .env   ← readable!
├─ .git/  vendor/
└─ public/

✓ Safe (body outside, only public)

app-laravel/   ← outside web root
├─ .env  .git/  ← unreachable
└─ public/ → required from public_html
Body outside the docroot, expose only public/. That alone makes .env unreachable.

Step 1: First aid (fast, reversible)

Add a deny block at the top of the web root's .htaccess (a defensive config example). Normal pages are unaffected.

# === SECURITY BLOCK ===
RedirectMatch 404 (?i)/\.(env|git)(\..*)?(/|$)
 
<FilesMatch "(?i)\.(sql|sql\.gz|bak|old|swp|save|orig|tgz)$|^credentials\.json$|\.bk[._]">
    Require all denied
</FilesMatch>
 
<FilesMatch "(?i)^(phpinfo|info)\.php$">
    Require all denied
</FilesMatch>
 
# if Laravel internals are visible at /<app>/storage/... etc.
RedirectMatch 403 (?i)^/[^/]+/(storage|bootstrap/cache|config|database|resources|routes|tests|vendor)(/|$)
# === END ===

First aid isn't the goal

A blacklist is whack-a-mole — you'll forget composer.lock, new filenames add new holes. Treat it as buying time until Step 2.

Step 2: Restructure (the permanent fix)

Move the app body outside the web root and leave a tiny entry point that just loads it.

# 1. Move the body outside the web root (same filesystem → mv is instant, atomic)
mv ~/example/public_html/app  ~/example/app-laravel
 
# 2. Put only a minimal entry in the web root
mkdir -p ~/example/public_html/sub
cat > ~/example/public_html/sub/index.php <<'PHP'
<?php require __DIR__ . '/../../app-laravel/public/index.php';
PHP
 
# 3. Copy Laravel public/.htaccess, symlink static assets
# 4. Clear config cache
rm -f ~/example/app-laravel/bootstrap/cache/{config,services,packages,routes-v7}.php

ITD's hard-won trap: bootstrap-redirect over symlink

Making public_html itself a symlink to public/ can return 500 on a registered subdomain's docroot in some environments (it doesn't follow). Keeping the docroot a real directory and require-ing by absolute path from a small index.php (the "bootstrap-redirect" shape) is more stable on scope-constrained shared hosting. Also, opcache can memorize a failed load and keep returning 500 — last resort is to change the entry filename (entry.php).

Step 3: Rotate keys (assume they were seen)

If there was any exposure window, treat the .env keys as already seen and replace them. Order: external API / OAuth secrets → encryption keys → mail → DB. See the .env glossary. Re-issuing an OAuth secret can invalidate refresh_tokens, so have a re-authorization flow ready first.

Step 4: Self-check (make it a habit)

Finally, confirm nothing is actually exposed on your own site (only against domains you own).

# A 200 with a body means it's exposed. 403/404 is okay for now.
curl -sI https://your-domain/.env | head -1
curl -sI https://your-domain/.git/config | head -1

Checking this on every deploy catches placement mistakes early.

FAQ

QWhat's the fastest thing I can do right now?
A

Add a deny block to your web root's .htaccess for .env, .git, SQL dumps, credentials.json, etc. It's reversible and doesn't affect normal pages — but it's first aid; the real fix is restructuring.

QWhy isn't .htaccess alone enough?
A

A blacklist is whack-a-mole — new filenames add new holes. Move the app body outside the web root and .env simply can't be reached, with no rules to maintain.

QWhy not just symlink public/ to expose it?
A

In some environments, re-pointing a registered subdomain's docroot to another tree via symlink doesn't follow and returns 500. In ITD's experience, keeping the docroot a real directory and `require`-ing by absolute path from a small index.php (the 'bootstrap-redirect' shape) is more stable.