Saltar al contenido
>_ITDITDPlataforma de seguridad web

Guías de seguridad

Mantener el .env fuera de la web pública en hosting compartido

Protege el .env en hosting compartido: cómo dejar .env, .git y vendor inalcanzables al desplegar apps tipo Laravel. La solución real es sacar la app de la raíz web y exponer solo public/. Primeros auxilios con .htaccess y reestructuración.

Publicado 2026-06-07 Actualizado 2026-06-07 4 min de lectura

Para: cualquiera que ejecute apps tipo Laravel (donde solo public/ debería estar expuesto) en hosting compartido con Apache/.htaccess. Destilado de un incidente real (→ .env expuesto al mundo) — para asegurar tu propio despliegue.

La visión de este sitio: esto no es 'el error de una persona'

La cultura temprana del hosting compartido ponía todo directamente bajo public_html. Laravel (que espera que solo se exponga public/) fluyó hacia eso, y un diseño peligroso se convirtió en el estándar de facto — incluso la propia documentación de los proveedores pone los proyectos dentro de public_html. Así que la solución es proceso, no vigilancia: cambia tu valor por defecto de despliegue.

Acierta con la ubicación (diagrama)

✗ Peligroso (cuerpo en la raíz pública)

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

✓ Seguro (cuerpo fuera, solo public)

app-laravel/   ← fuera de la raíz web
├─ .env  .git/  ← inalcanzables
└─ public/ → requerido desde public_html
Cuerpo fuera del docroot, exponer solo public/. Eso por sí solo hace inalcanzable el .env.

Paso 1: Primeros auxilios (rápido, reversible)

Añade un bloque de denegación al principio del .htaccess de la raíz web (un ejemplo de configuración defensiva). Las páginas normales no se ven afectadas.

# === 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>
 
# si las partes internas de Laravel son visibles en /<app>/storage/... etc.
RedirectMatch 403 (?i)^/[^/]+/(storage|bootstrap/cache|config|database|resources|routes|tests|vendor)(/|$)
# === END ===

Los primeros auxilios no son el objetivo

Una lista negra es un juego del topo — olvidarás composer.lock, los nombres de archivo nuevos añaden agujeros nuevos. Trátalo como ganar tiempo hasta el Paso 2.

Paso 2: Reestructurar (la solución permanente)

Mueve el cuerpo de la app fuera de la raíz web y deja un punto de entrada diminuto que solo lo cargue.

# 1. Mueve el cuerpo fuera de la raíz web (mismo sistema de archivos → mv es instantáneo, atómico)
mv ~/example/public_html/app  ~/example/app-laravel
 
# 2. Pon solo una entrada mínima en la raíz web
mkdir -p ~/example/public_html/sub
cat > ~/example/public_html/sub/index.php <<'PHP'
<?php require __DIR__ . '/../../app-laravel/public/index.php';
PHP
 
# 3. Copia el public/.htaccess de Laravel, haz symlink de los recursos estáticos
# 4. Limpia la caché de configuración
rm -f ~/example/app-laravel/bootstrap/cache/{config,services,packages,routes-v7}.php

La trampa que este sitio aprendió a las malas: bootstrap-redirect en lugar de symlink

Hacer del propio public_html un symlink a public/ puede devolver 500 en el docroot de un subdominio registrado en algunos entornos (no se sigue). Mantener el docroot como un directorio real y hacer require por ruta absoluta desde un pequeño index.php (la forma "bootstrap-redirect") es más estable en hosting compartido con alcance restringido. Además, opcache puede memorizar una carga fallida y seguir devolviendo 500 — el último recurso es cambiar el nombre del archivo de entrada (entry.php).

Paso 3: Rota las claves (asume que fueron vistas)

Si hubo cualquier ventana de exposición, trata las claves del .env como ya vistas y reemplázalas. Orden: secretos de API externa / OAuth → claves de cifrado → correo → BD. Ver el glosario del .env. Reemitir un secreto de OAuth puede invalidar los refresh_token, así que ten lista primero una vía de reautorización.

Paso 4: Autocomprobación (hazlo un hábito)

Por último, confirma que nada esté de verdad expuesto en tu propio sitio (solo contra dominios que poseas).

# Un 200 con cuerpo significa que está expuesto. 403/404 está bien por ahora.
curl -sI https://your-domain/.env | head -1
curl -sI https://your-domain/.git/config | head -1

Comprobar esto en cada despliegue detecta pronto los errores de ubicación.

Sigue leyendo

FAQ

Q¿Qué es lo más rápido que puedo hacer ahora mismo?
A

Añade un bloque de denegación al .htaccess de tu raíz web para .env, .git, volcados SQL, credentials.json, etc. Es reversible y no afecta a las páginas normales — pero son primeros auxilios; la solución real es reestructurar.

Q¿Por qué no basta con el .htaccess solo?
A

Una lista negra es un juego del topo — los nombres de archivo nuevos añaden agujeros nuevos. Mueve el cuerpo de la app fuera de la raíz web y el .env simplemente no se puede alcanzar, sin reglas que mantener.

Q¿Por qué no hacer simplemente un symlink de public/ para exponerlo?
A

En algunos entornos, reapuntar el docroot de un subdominio registrado a otro árbol vía symlink no se sigue y devuelve 500. Según la experiencia de este sitio, mantener el docroot como un directorio real y hacer `require` por ruta absoluta desde un pequeño index.php (la forma 'bootstrap-redirect') es más estable.